From f67ef2689f26992896e4ed7ecf24b28ebc485640 Mon Sep 17 00:00:00 2001 From: ilhom Date: Mon, 22 Jun 2026 14:17:49 +0700 Subject: [PATCH 1/9] feat(finance): regenerate TypeScript types for bulk product routing RPCs Co-Authored-By: Claude Sonnet 4.6 --- src/types/generated/finance/v1/cost_import.ts | 1033 +++++++++++++++-- 1 file changed, 967 insertions(+), 66 deletions(-) diff --git a/src/types/generated/finance/v1/cost_import.ts b/src/types/generated/finance/v1/cost_import.ts index 8180c78..53c571b 100644 --- a/src/types/generated/finance/v1/cost_import.ts +++ b/src/types/generated/finance/v1/cost_import.ts @@ -107,6 +107,55 @@ export interface DownloadCostProductParameterTemplateResponse { fileName: string; } +export interface ImportRowError { + rowNumber: number; + field: string; + message: string; +} + +export interface BulkSheetValidationResult { + sheetName: string; + totalRows: number; + errorCount: number; + warningCount: number; + sampleErrors: ImportRowError[]; +} + +export interface ImportBulkProductRoutingRequest { + fileContent: Uint8Array; + fileName: string; + duplicateAction: string; +} + +export interface ImportBulkProductRoutingResponse { + base: BaseResponse | undefined; + jobId: number; + status: string; +} + +export interface ValidateBulkProductRoutingFileRequest { + fileContent: Uint8Array; + fileName: string; +} + +export interface ValidateBulkProductRoutingFileResponse { + base: BaseResponse | undefined; + isValid: boolean; + sheets: BulkSheetValidationResult[]; +} + +export interface ExportBulkProductRoutingRequest { + productTypeCodes: string[]; + includeRouting: boolean; + activeOnly: boolean; +} + +export interface ExportBulkProductRoutingResponse { + base: BaseResponse | undefined; + jobId: number; + status: string; +} + function createBaseCostImportJob(): CostImportJob { return { jobId: 0, @@ -1743,72 +1792,924 @@ export const DownloadCostProductParameterTemplateResponse: MessageFns = { + encode(message: ImportRowError, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.rowNumber !== 0) { + writer.uint32(8).int32(message.rowNumber); + } + if (message.field !== "") { + writer.uint32(18).string(message.field); + } + if (message.message !== "") { + writer.uint32(26).string(message.message); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ImportRowError { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseImportRowError(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.rowNumber = reader.int32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.field = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.message = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ImportRowError { + return { + rowNumber: isSet(object.rowNumber) + ? globalThis.Number(object.rowNumber) + : isSet(object.row_number) + ? globalThis.Number(object.row_number) + : 0, + field: isSet(object.field) ? globalThis.String(object.field) : "", + message: isSet(object.message) ? globalThis.String(object.message) : "", + }; + }, + + toJSON(message: ImportRowError): unknown { + const obj: any = {}; + if (message.rowNumber !== 0) { + obj.rowNumber = Math.round(message.rowNumber); + } + if (message.field !== "") { + obj.field = message.field; + } + if (message.message !== "") { + obj.message = message.message; + } + return obj; + }, + + create(base?: DeepPartial): ImportRowError { + return ImportRowError.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ImportRowError { + const message = createBaseImportRowError(); + message.rowNumber = object.rowNumber ?? 0; + message.field = object.field ?? ""; + message.message = object.message ?? ""; + return message; + }, +}; + +function createBaseBulkSheetValidationResult(): BulkSheetValidationResult { + return { sheetName: "", totalRows: 0, errorCount: 0, warningCount: 0, sampleErrors: [] }; +} + +export const BulkSheetValidationResult: MessageFns = { + encode(message: BulkSheetValidationResult, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.sheetName !== "") { + writer.uint32(10).string(message.sheetName); + } + if (message.totalRows !== 0) { + writer.uint32(16).int32(message.totalRows); + } + if (message.errorCount !== 0) { + writer.uint32(24).int32(message.errorCount); + } + if (message.warningCount !== 0) { + writer.uint32(32).int32(message.warningCount); + } + for (const v of message.sampleErrors) { + ImportRowError.encode(v!, writer.uint32(42).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): BulkSheetValidationResult { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseBulkSheetValidationResult(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.sheetName = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.totalRows = reader.int32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.errorCount = reader.int32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.warningCount = reader.int32(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.sampleErrors.push(ImportRowError.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): BulkSheetValidationResult { + return { + sheetName: isSet(object.sheetName) + ? globalThis.String(object.sheetName) + : isSet(object.sheet_name) + ? globalThis.String(object.sheet_name) + : "", + totalRows: isSet(object.totalRows) + ? globalThis.Number(object.totalRows) + : isSet(object.total_rows) + ? globalThis.Number(object.total_rows) + : 0, + errorCount: isSet(object.errorCount) + ? globalThis.Number(object.errorCount) + : isSet(object.error_count) + ? globalThis.Number(object.error_count) + : 0, + warningCount: isSet(object.warningCount) + ? globalThis.Number(object.warningCount) + : isSet(object.warning_count) + ? globalThis.Number(object.warning_count) + : 0, + sampleErrors: globalThis.Array.isArray(object?.sampleErrors) + ? object.sampleErrors.map((e: any) => ImportRowError.fromJSON(e)) + : globalThis.Array.isArray(object?.sample_errors) + ? object.sample_errors.map((e: any) => ImportRowError.fromJSON(e)) + : [], + }; + }, + + toJSON(message: BulkSheetValidationResult): unknown { + const obj: any = {}; + if (message.sheetName !== "") { + obj.sheetName = message.sheetName; + } + if (message.totalRows !== 0) { + obj.totalRows = Math.round(message.totalRows); + } + if (message.errorCount !== 0) { + obj.errorCount = Math.round(message.errorCount); + } + if (message.warningCount !== 0) { + obj.warningCount = Math.round(message.warningCount); + } + if (message.sampleErrors?.length) { + obj.sampleErrors = message.sampleErrors.map((e) => ImportRowError.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): BulkSheetValidationResult { + return BulkSheetValidationResult.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): BulkSheetValidationResult { + const message = createBaseBulkSheetValidationResult(); + message.sheetName = object.sheetName ?? ""; + message.totalRows = object.totalRows ?? 0; + message.errorCount = object.errorCount ?? 0; + message.warningCount = object.warningCount ?? 0; + message.sampleErrors = object.sampleErrors?.map((e) => ImportRowError.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseImportBulkProductRoutingRequest(): ImportBulkProductRoutingRequest { + return { fileContent: new Uint8Array(0), fileName: "", duplicateAction: "" }; +} + +export const ImportBulkProductRoutingRequest: MessageFns = { + encode(message: ImportBulkProductRoutingRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.fileContent.length !== 0) { + writer.uint32(10).bytes(message.fileContent); + } + if (message.fileName !== "") { + writer.uint32(18).string(message.fileName); + } + if (message.duplicateAction !== "") { + writer.uint32(26).string(message.duplicateAction); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ImportBulkProductRoutingRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseImportBulkProductRoutingRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.fileContent = reader.bytes(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.fileName = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.duplicateAction = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ImportBulkProductRoutingRequest { + return { + fileContent: isSet(object.fileContent) + ? bytesFromBase64(object.fileContent) + : isSet(object.file_content) + ? bytesFromBase64(object.file_content) + : new Uint8Array(0), + fileName: isSet(object.fileName) + ? globalThis.String(object.fileName) + : isSet(object.file_name) + ? globalThis.String(object.file_name) + : "", + duplicateAction: isSet(object.duplicateAction) + ? globalThis.String(object.duplicateAction) + : isSet(object.duplicate_action) + ? globalThis.String(object.duplicate_action) + : "", + }; + }, + + toJSON(message: ImportBulkProductRoutingRequest): unknown { + const obj: any = {}; + if (message.fileContent.length !== 0) { + obj.fileContent = base64FromBytes(message.fileContent); + } + if (message.fileName !== "") { + obj.fileName = message.fileName; + } + if (message.duplicateAction !== "") { + obj.duplicateAction = message.duplicateAction; + } + return obj; + }, + + create(base?: DeepPartial): ImportBulkProductRoutingRequest { + return ImportBulkProductRoutingRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ImportBulkProductRoutingRequest { + const message = createBaseImportBulkProductRoutingRequest(); + message.fileContent = object.fileContent ?? new Uint8Array(0); + message.fileName = object.fileName ?? ""; + message.duplicateAction = object.duplicateAction ?? ""; + return message; + }, +}; + +function createBaseImportBulkProductRoutingResponse(): ImportBulkProductRoutingResponse { + return { base: undefined, jobId: 0, status: "" }; +} + +export const ImportBulkProductRoutingResponse: MessageFns = { + encode(message: ImportBulkProductRoutingResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + if (message.jobId !== 0) { + writer.uint32(16).int64(message.jobId); + } + if (message.status !== "") { + writer.uint32(26).string(message.status); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ImportBulkProductRoutingResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseImportBulkProductRoutingResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.base = BaseResponse.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.jobId = longToNumber(reader.int64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.status = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ImportBulkProductRoutingResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + jobId: isSet(object.jobId) + ? globalThis.Number(object.jobId) + : isSet(object.job_id) + ? globalThis.Number(object.job_id) + : 0, + status: isSet(object.status) ? globalThis.String(object.status) : "", + }; + }, + + toJSON(message: ImportBulkProductRoutingResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.jobId !== 0) { + obj.jobId = Math.round(message.jobId); + } + if (message.status !== "") { + obj.status = message.status; + } + return obj; + }, + + create(base?: DeepPartial): ImportBulkProductRoutingResponse { + return ImportBulkProductRoutingResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ImportBulkProductRoutingResponse { + const message = createBaseImportBulkProductRoutingResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.jobId = object.jobId ?? 0; + message.status = object.status ?? ""; + return message; + }, +}; + +function createBaseValidateBulkProductRoutingFileRequest(): ValidateBulkProductRoutingFileRequest { + return { fileContent: new Uint8Array(0), fileName: "" }; +} + +export const ValidateBulkProductRoutingFileRequest: MessageFns = { + encode(message: ValidateBulkProductRoutingFileRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.fileContent.length !== 0) { + writer.uint32(10).bytes(message.fileContent); + } + if (message.fileName !== "") { + writer.uint32(18).string(message.fileName); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ValidateBulkProductRoutingFileRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseValidateBulkProductRoutingFileRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.fileContent = reader.bytes(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.fileName = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ValidateBulkProductRoutingFileRequest { + return { + fileContent: isSet(object.fileContent) + ? bytesFromBase64(object.fileContent) + : isSet(object.file_content) + ? bytesFromBase64(object.file_content) + : new Uint8Array(0), + fileName: isSet(object.fileName) + ? globalThis.String(object.fileName) + : isSet(object.file_name) + ? globalThis.String(object.file_name) + : "", + }; + }, + + toJSON(message: ValidateBulkProductRoutingFileRequest): unknown { + const obj: any = {}; + if (message.fileContent.length !== 0) { + obj.fileContent = base64FromBytes(message.fileContent); + } + if (message.fileName !== "") { + obj.fileName = message.fileName; + } + return obj; + }, + + create(base?: DeepPartial): ValidateBulkProductRoutingFileRequest { + return ValidateBulkProductRoutingFileRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ValidateBulkProductRoutingFileRequest { + const message = createBaseValidateBulkProductRoutingFileRequest(); + message.fileContent = object.fileContent ?? new Uint8Array(0); + message.fileName = object.fileName ?? ""; + return message; + }, +}; + +function createBaseValidateBulkProductRoutingFileResponse(): ValidateBulkProductRoutingFileResponse { + return { base: undefined, isValid: false, sheets: [] }; +} + +export const ValidateBulkProductRoutingFileResponse: MessageFns = { + encode(message: ValidateBulkProductRoutingFileResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + if (message.isValid !== false) { + writer.uint32(16).bool(message.isValid); + } + for (const v of message.sheets) { + BulkSheetValidationResult.encode(v!, writer.uint32(26).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ValidateBulkProductRoutingFileResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseValidateBulkProductRoutingFileResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.base = BaseResponse.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.isValid = reader.bool(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.sheets.push(BulkSheetValidationResult.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ValidateBulkProductRoutingFileResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + isValid: isSet(object.isValid) + ? globalThis.Boolean(object.isValid) + : isSet(object.is_valid) + ? globalThis.Boolean(object.is_valid) + : false, + sheets: globalThis.Array.isArray(object?.sheets) + ? object.sheets.map((e: any) => BulkSheetValidationResult.fromJSON(e)) + : [], + }; + }, + + toJSON(message: ValidateBulkProductRoutingFileResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.isValid !== false) { + obj.isValid = message.isValid; + } + if (message.sheets?.length) { + obj.sheets = message.sheets.map((e) => BulkSheetValidationResult.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): ValidateBulkProductRoutingFileResponse { + return ValidateBulkProductRoutingFileResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ValidateBulkProductRoutingFileResponse { + const message = createBaseValidateBulkProductRoutingFileResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.isValid = object.isValid ?? false; + message.sheets = object.sheets?.map((e) => BulkSheetValidationResult.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseExportBulkProductRoutingRequest(): ExportBulkProductRoutingRequest { + return { productTypeCodes: [], includeRouting: false, activeOnly: false }; +} + +export const ExportBulkProductRoutingRequest: MessageFns = { + encode(message: ExportBulkProductRoutingRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.productTypeCodes) { + writer.uint32(10).string(v!); + } + if (message.includeRouting !== false) { + writer.uint32(16).bool(message.includeRouting); + } + if (message.activeOnly !== false) { + writer.uint32(24).bool(message.activeOnly); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ExportBulkProductRoutingRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExportBulkProductRoutingRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.productTypeCodes.push(reader.string()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.includeRouting = reader.bool(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.activeOnly = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ExportBulkProductRoutingRequest { + return { + productTypeCodes: globalThis.Array.isArray(object?.productTypeCodes) + ? object.productTypeCodes.map((e: any) => globalThis.String(e)) + : globalThis.Array.isArray(object?.product_type_codes) + ? object.product_type_codes.map((e: any) => globalThis.String(e)) + : [], + includeRouting: isSet(object.includeRouting) + ? globalThis.Boolean(object.includeRouting) + : isSet(object.include_routing) + ? globalThis.Boolean(object.include_routing) + : false, + activeOnly: isSet(object.activeOnly) + ? globalThis.Boolean(object.activeOnly) + : isSet(object.active_only) + ? globalThis.Boolean(object.active_only) + : false, + }; + }, + + toJSON(message: ExportBulkProductRoutingRequest): unknown { + const obj: any = {}; + if (message.productTypeCodes?.length) { + obj.productTypeCodes = message.productTypeCodes; + } + if (message.includeRouting !== false) { + obj.includeRouting = message.includeRouting; + } + if (message.activeOnly !== false) { + obj.activeOnly = message.activeOnly; + } + return obj; + }, + + create(base?: DeepPartial): ExportBulkProductRoutingRequest { + return ExportBulkProductRoutingRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ExportBulkProductRoutingRequest { + const message = createBaseExportBulkProductRoutingRequest(); + message.productTypeCodes = object.productTypeCodes?.map((e) => e) || []; + message.includeRouting = object.includeRouting ?? false; + message.activeOnly = object.activeOnly ?? false; + return message; + }, +}; + +function createBaseExportBulkProductRoutingResponse(): ExportBulkProductRoutingResponse { + return { base: undefined, jobId: 0, status: "" }; +} + +export const ExportBulkProductRoutingResponse: MessageFns = { + encode(message: ExportBulkProductRoutingResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + if (message.jobId !== 0) { + writer.uint32(16).int64(message.jobId); + } + if (message.status !== "") { + writer.uint32(26).string(message.status); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ExportBulkProductRoutingResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExportBulkProductRoutingResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.base = BaseResponse.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.jobId = longToNumber(reader.int64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.status = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ExportBulkProductRoutingResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + jobId: isSet(object.jobId) + ? globalThis.Number(object.jobId) + : isSet(object.job_id) + ? globalThis.Number(object.job_id) + : 0, + status: isSet(object.status) ? globalThis.String(object.status) : "", + }; + }, + + toJSON(message: ExportBulkProductRoutingResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.jobId !== 0) { + obj.jobId = Math.round(message.jobId); + } + if (message.status !== "") { + obj.status = message.status; + } + return obj; + }, + + create(base?: DeepPartial): ExportBulkProductRoutingResponse { + return ExportBulkProductRoutingResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ExportBulkProductRoutingResponse { + const message = createBaseExportBulkProductRoutingResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.jobId = object.jobId ?? 0; + message.status = object.status ?? ""; + return message; + }, +}; + +export type CostDataImportServiceDefinition = typeof CostDataImportServiceDefinition; +export const CostDataImportServiceDefinition = { + name: "CostDataImportService", + fullName: "finance.v1.CostDataImportService", + methods: { + getCostImportJob: { + name: "GetCostImportJob", + requestType: GetCostImportJobRequest, + requestStream: false, + responseType: GetCostImportJobResponse, + responseStream: false, + options: {}, + }, + listCostImportJobs: { + name: "ListCostImportJobs", + requestType: ListCostImportJobsRequest, + requestStream: false, + responseType: ListCostImportJobsResponse, + responseStream: false, + options: {}, + }, + importCostApplicableParams: { + name: "ImportCostApplicableParams", + requestType: ImportCostApplicableParamsRequest, + requestStream: false, + responseType: ImportCostApplicableParamsResponse, + responseStream: false, + options: {}, + }, + exportCostApplicableParams: { + name: "ExportCostApplicableParams", + requestType: ExportCostApplicableParamsRequest, + requestStream: false, + responseType: ExportCostApplicableParamsResponse, + responseStream: false, + options: {}, + }, + downloadCostApplicableParamTemplate: { + name: "DownloadCostApplicableParamTemplate", + requestType: DownloadCostApplicableParamTemplateRequest, + requestStream: false, + responseType: DownloadCostApplicableParamTemplateResponse, + responseStream: false, + options: {}, + }, + importCostProductParameters: { + name: "ImportCostProductParameters", + requestType: ImportCostProductParametersRequest, + requestStream: false, + responseType: ImportCostProductParametersResponse, + responseStream: false, + options: {}, + }, + exportCostProductParameters: { + name: "ExportCostProductParameters", + requestType: ExportCostProductParametersRequest, + requestStream: false, + responseType: ExportCostProductParametersResponse, + responseStream: false, + options: {}, + }, + downloadCostProductParameterTemplate: { + name: "DownloadCostProductParameterTemplate", + requestType: DownloadCostProductParameterTemplateRequest, + requestStream: false, + responseType: DownloadCostProductParameterTemplateResponse, + responseStream: false, + options: {}, + }, + importBulkProductRouting: { + name: "ImportBulkProductRouting", + requestType: ImportBulkProductRoutingRequest, + requestStream: false, + responseType: ImportBulkProductRoutingResponse, + responseStream: false, + options: {}, + }, + validateBulkProductRoutingFile: { + name: "ValidateBulkProductRoutingFile", + requestType: ValidateBulkProductRoutingFileRequest, + requestStream: false, + responseType: ValidateBulkProductRoutingFileResponse, + responseStream: false, + options: {}, + }, + exportBulkProductRouting: { + name: "ExportBulkProductRouting", + requestType: ExportBulkProductRoutingRequest, + requestStream: false, + responseType: ExportBulkProductRoutingResponse, responseStream: false, options: {}, }, From ce44b23807ee8a2a53e3728ab8626d3034297739 Mon Sep 17 00:00:00 2001 From: ilhom Date: Mon, 22 Jun 2026 16:24:49 +0700 Subject: [PATCH 2/9] feat(frontend): add bulk product routing import dialog, BFF routes, and page integration Co-Authored-By: Claude Sonnet 4.6 --- .../product-master-page-client.tsx | 7 + .../export/bulk_product_routing/route.ts | 52 +++ .../import/bulk_product_routing/route.ts | 46 +++ .../validate/bulk_product_routing/route.ts | 55 +++ .../finance/costing/bulk-import-dialog.tsx | 331 ++++++++++++++++++ src/services/finance/cost-import-api.ts | 100 ++++++ src/types/finance/cost-import.ts | 43 +++ 7 files changed, 634 insertions(+) create mode 100644 src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts create mode 100644 src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts create mode 100644 src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts create mode 100644 src/components/finance/costing/bulk-import-dialog.tsx diff --git a/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx b/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx index 4ddef85..ea596b8 100644 --- a/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx +++ b/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx @@ -18,6 +18,7 @@ import { ProductMasterTable, } from "@/components/finance/cost-product-master" import { ImportExportToolbar } from "@/components/finance/costing/import-export-toolbar" +import { BulkImportDialog, BulkExportButton } from "@/components/finance/costing/bulk-import-dialog" import { ProductTypeCombobox } from "@/components/finance/comboboxes" import { DataTablePagination } from "@/components/shared" import { useCostProductMasterCounts, useCostProductMasters, costProductMasterKeys } from "@/hooks/finance/use-cost-product-master" @@ -41,6 +42,7 @@ export default function ProductMasterPageClient() { const [formOpen, setFormOpen] = useState(false) const [erpOpen, setErpOpen] = useState(false) const [deactivateOpen, setDeactivateOpen] = useState(false) + const [bulkImportOpen, setBulkImportOpen] = useState(false) const [editing, setEditing] = useState(null) function openCreate() { @@ -76,6 +78,10 @@ export default function ProductMasterPageClient() { queryClient.invalidateQueries({ queryKey: costProductMasterKeys.all }) } /> + + @@ -152,6 +158,7 @@ export default function ProductMasterPageClient() { onOpenChange={setDeactivateOpen} product={editing} /> + ) } diff --git a/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts new file mode 100644 index 0000000..588ee69 --- /dev/null +++ b/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts @@ -0,0 +1,52 @@ +// POST /api/v1/finance/costing/export/bulk_product_routing +// Queue an async export of product master + routing data to MinIO. +// Accepts JSON body: { productTypeCodes?: string[] } +// Returns: { jobId: number, status: string } + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const productTypeCodes: string[] = Array.isArray(body.productTypeCodes) + ? (body.productTypeCodes as string[]) + : [] + + const includeRouting: boolean = + typeof body.includeRouting === "boolean" ? body.includeRouting : true + const activeOnly: boolean = + typeof body.activeOnly === "boolean" ? body.activeOnly : false + + const res = await getCostDataImportClient().exportBulkProductRouting( + { productTypeCodes, includeRouting, activeOnly }, + metadata, + ) + + return NextResponse.json({ + base: res.base, + jobId: res.jobId, + status: res.status, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error exporting bulk product routing:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to export bulk product routing", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts new file mode 100644 index 0000000..8c66313 --- /dev/null +++ b/src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts @@ -0,0 +1,46 @@ +// POST /api/v1/finance/costing/import/bulk_product_routing +// Import product master + routing data from an Excel file. +// Accepts JSON body: { fileContent: number[], fileName: string, duplicateAction?: string } +// Returns: { base, data: { jobId: number } } + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const fileContent = new Uint8Array(body.fileContent as number[]) + const fileName: string = body.fileName ?? "" + const duplicateAction: string = body.duplicateAction ?? "update" + + const res = await getCostDataImportClient().importBulkProductRouting( + { fileContent, fileName, duplicateAction }, + metadata, + ) + + return NextResponse.json({ + base: res.base, + data: { jobId: res.jobId, status: res.status }, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error importing bulk product routing:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to import bulk product routing", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts new file mode 100644 index 0000000..a52dbd8 --- /dev/null +++ b/src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts @@ -0,0 +1,55 @@ +// POST /api/v1/finance/costing/validate/bulk_product_routing +// Validate a bulk product routing Excel file before importing. +// Accepts JSON body: { fileContent: number[], fileName: string } +// Returns: { isValid: boolean, sheets: BulkSheetValidationResult[] } + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const fileContent = new Uint8Array(body.fileContent as number[]) + const fileName: string = body.fileName ?? "" + + const res = await getCostDataImportClient().validateBulkProductRoutingFile( + { fileContent, fileName }, + metadata, + ) + + return NextResponse.json({ + isValid: res.isValid, + sheets: res.sheets.map((sheet) => ({ + sheetName: sheet.sheetName, + totalRows: sheet.totalRows, + errorCount: sheet.errorCount, + warningCount: sheet.warningCount, + sampleErrors: sheet.sampleErrors.map((e) => ({ + rowNumber: e.rowNumber, + field: e.field, + message: e.message, + })), + })), + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error validating bulk product routing file:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to validate bulk product routing file", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/components/finance/costing/bulk-import-dialog.tsx b/src/components/finance/costing/bulk-import-dialog.tsx new file mode 100644 index 0000000..8ccc73d --- /dev/null +++ b/src/components/finance/costing/bulk-import-dialog.tsx @@ -0,0 +1,331 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { AlertCircle, CheckCircle2, ChevronDown, ChevronRight, FileSpreadsheet, Loader2, Upload } from "lucide-react" +import { toast } from "sonner" + +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + bulkImportProductMasterRouting, + exportBulkProductRouting, + validateBulkProductRoutingFile, +} from "@/services/finance/cost-import-api" +import type { BulkSheetValidationResult, BulkValidationResult } from "@/types/finance/cost-import" + +export interface BulkImportDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +type Step = "upload" | "validating" | "validated" | "submitting" | "done" + +export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) { + const fileRef = useRef(null) + const [file, setFile] = useState(null) + const [step, setStep] = useState("upload") + const [validation, setValidation] = useState(null) + const [expandedSheets, setExpandedSheets] = useState>(new Set()) + const [jobId, setJobId] = useState(null) + + // Stable reset function — called from handleClose and when open changes + function resetState() { + setFile(null) + setStep("upload") + setValidation(null) + setExpandedSheets(new Set()) + setJobId(null) + if (fileRef.current) fileRef.current.value = "" + } + + // Reset all state when dialog opens + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { if (open) resetState() }, [open]) + + function handleFileChange(e: React.ChangeEvent) { + const selected = e.target.files?.[0] ?? null + setFile(selected) + setValidation(null) + setStep("upload") + } + + async function handleValidate() { + if (!file) return + setStep("validating") + try { + const result = await validateBulkProductRoutingFile(file) + setValidation(result) + setStep("validated") + } catch (e) { + toast.error(`Validation failed: ${String(e)}`) + setStep("upload") + } + } + + async function handleImport() { + if (!file) return + setStep("submitting") + try { + const result = await bulkImportProductMasterRouting(file, "update") + setJobId(result.jobId) + setStep("done") + toast.success(`Import queued — Job #${result.jobId}`) + } catch (e) { + toast.error(`Import failed: ${String(e)}`) + setStep("validated") + } + } + + function handleClose() { + resetState() + onOpenChange(false) + } + + function toggleSheet(sheetName: string) { + setExpandedSheets((prev) => { + const next = new Set(prev) + if (next.has(sheetName)) { + next.delete(sheetName) + } else { + next.add(sheetName) + } + return next + }) + } + + const hasErrors = validation?.sheets.some((s) => s.errorCount > 0) ?? false + const isValidating = step === "validating" + const isSubmitting = step === "submitting" + const isDone = step === "done" + + return ( + + + + Bulk Import — Product Master & Routing + + +
+ {/* Step 1 — File upload */} +
+
fileRef.current?.click()} + > + + {file ? ( +
+ + {file.name} + + ({(file.size / 1024).toFixed(1)} KB) + +
+ ) : ( + <> + +

+ Click to select an Excel file (.xlsx) +

+ + )} +
+
+ + {/* Step 2 — Validation loading */} + {isValidating && ( +
+ + Validating file… +
+ )} + + {/* Step 2 — Validation results */} + {validation && !isValidating && ( +
+
+ {validation.isValid ? ( + + ) : ( + + )} + + {validation.isValid + ? "File is valid — ready to import" + : "Validation failed — fix errors before importing"} + +
+ + + + + + Sheet + Rows + Errors + Warnings + + + + + {validation.sheets.map((sheet) => ( + <> + 0 ? "bg-destructive/5" : undefined} + > + {sheet.sheetName} + {sheet.totalRows} + + {sheet.errorCount > 0 ? ( + + {sheet.errorCount} + + ) : ( + 0 + )} + + + {sheet.warningCount > 0 ? ( + + {sheet.warningCount} + + ) : ( + 0 + )} + + + {sheet.sampleErrors.length > 0 && ( + + )} + + + + {/* Sample errors for this sheet */} + {expandedSheets.has(sheet.sheetName) && + sheet.sampleErrors.map((err, i) => ( + + + Row {err.rowNumber}{err.field ? ` [${err.field}]` : ""}: {err.message} + + + ))} + + ))} + +
+
+
+ )} + + {/* Step 3 — Done */} + {isDone && jobId !== null && ( +
+ + Import queued as Job #{jobId}. You will receive a notification when it + completes. +
+ )} +
+ + + + + {/* Validate button — shown when file selected but not yet validated */} + {file && step === "upload" && ( + + )} + + {/* Start Import button — shown only when validation passed */} + {step === "validated" && !hasErrors && ( + + )} + +
+
+ ) +} + +/** + * Standalone "Export All" button that queues an async bulk export job. + */ +export function BulkExportButton({ + productTypeCodes, +}: { + productTypeCodes?: string[] +}) { + const [loading, setLoading] = useState(false) + + async function handleExport() { + setLoading(true) + try { + const result = await exportBulkProductRouting({ productTypeCodes }) + toast.success(`Export queued — Job #${result.jobId}`) + } catch (e) { + toast.error(`Export failed: ${String(e)}`) + } finally { + setLoading(false) + } + } + + return ( + + ) +} + +// Re-export BulkSheetValidationResult for any consumers that import from this module +export type { BulkSheetValidationResult, BulkValidationResult } diff --git a/src/services/finance/cost-import-api.ts b/src/services/finance/cost-import-api.ts index 76f9c28..f0ab0be 100644 --- a/src/services/finance/cost-import-api.ts +++ b/src/services/finance/cost-import-api.ts @@ -5,6 +5,9 @@ import type { SyncImportResult, AsyncImportResponse, ImportEntity, + BulkValidationResult, + BulkSheetValidationResult, + BulkRowError, } from "@/types/finance/cost-import" const BASE = "/api/v1/finance/costing" @@ -102,3 +105,100 @@ export async function getImportJob(jobId: number): Promise { const json = await res.json() return json.data as CostImportJob } + +// ── Bulk Product Routing ──────────────────────────────────────────────────── + +/** + * Queue a bulk import of product master + routing data from an Excel file. + * Returns a job ID that can be polled with getImportJob(). + */ +export async function bulkImportProductMasterRouting( + file: File, + duplicateAction?: string, +): Promise<{ jobId: number; status: string }> { + const fileContent = Array.from(new Uint8Array(await file.arrayBuffer())) + const res = await fetch(`${BASE}/import/bulk_product_routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fileContent, + fileName: file.name, + duplicateAction: duplicateAction ?? "update", + }), + }) + if (!res.ok) throw new Error(`Bulk import failed: ${res.status}`) + const json = await res.json() + return { + jobId: json.data?.jobId ?? json.data?.job_id ?? 0, + status: json.data?.status ?? "", + } +} + +/** + * Validate a bulk product routing Excel file without importing. + * Returns a per-sheet summary with error/warning counts and sample errors. + */ +export async function validateBulkProductRoutingFile( + file: File, +): Promise { + const fileContent = Array.from(new Uint8Array(await file.arrayBuffer())) + const res = await fetch(`${BASE}/validate/bulk_product_routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileContent, fileName: file.name }), + }) + if (!res.ok) throw new Error(`Validation failed: ${res.status}`) + const json = await res.json() + + // Normalise: handle both camelCase (gRPC-gateway) and snake_case (raw proto) + const isValid: boolean = json.isValid ?? json.is_valid ?? false + const rawSheets: unknown[] = Array.isArray(json.sheets) ? json.sheets : [] + + const sheets: BulkSheetValidationResult[] = rawSheets.map((s) => { + const sheet = s as Record + const rawErrors: unknown[] = Array.isArray(sheet.sampleErrors) + ? (sheet.sampleErrors as unknown[]) + : Array.isArray(sheet.sample_errors) + ? (sheet.sample_errors as unknown[]) + : [] + + const sampleErrors: BulkRowError[] = rawErrors.map((e) => { + const err = e as Record + return { + rowNumber: Number(err.rowNumber ?? err.row_number ?? 0), + field: String(err.field ?? ""), + message: String(err.message ?? ""), + } + }) + + return { + sheetName: String(sheet.sheetName ?? sheet.sheet_name ?? ""), + totalRows: Number(sheet.totalRows ?? sheet.total_rows ?? 0), + errorCount: Number(sheet.errorCount ?? sheet.error_count ?? 0), + warningCount: Number(sheet.warningCount ?? sheet.warning_count ?? 0), + sampleErrors, + } + }) + + return { isValid, sheets } +} + +/** + * Queue an async export of all product master + routing data to MinIO. + * Returns a job ID that can be polled or viewed on the import-jobs page. + */ +export async function exportBulkProductRouting(options?: { + productTypeCodes?: string[] +}): Promise<{ jobId: number; status: string }> { + const res = await fetch(`${BASE}/export/bulk_product_routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ productTypeCodes: options?.productTypeCodes ?? [] }), + }) + if (!res.ok) throw new Error(`Bulk export failed: ${res.status}`) + const json = await res.json() + return { + jobId: json.jobId ?? json.job_id ?? json.data?.jobId ?? json.data?.job_id ?? 0, + status: json.status ?? json.data?.status ?? "", + } +} diff --git a/src/types/finance/cost-import.ts b/src/types/finance/cost-import.ts index 7155f70..b50c935 100644 --- a/src/types/finance/cost-import.ts +++ b/src/types/finance/cost-import.ts @@ -77,3 +77,46 @@ export function normalizeCostImportJob(raw: RawCostImportJob): CostImportJob { completedAt: raw.completedAt ?? raw.completed_at ?? "", } } + +// ── Import entity label map ───────────────────────────────────────────────── + +/** Human-readable labels for all import/export entity types. */ +export const IMPORT_ENTITY_LABELS: Record = { + product_type: "Product Type", + parameter: "Parameter", + product_master: "Product Master", + capp: "Cost Applicable Parameters", + cpp: "Cost Product Parameters", + bulk_product_routing: "Bulk Import (Product Master + Routing)", + bulk_product_routing_export: "Bulk Export (Product Master + Routing)", +} + +// ── Bulk Product Routing import/export ───────────────────────────────────── + +/** Entity constant for the bulk import (product master + routing). */ +export const ENTITY_BULK_PRODUCT_ROUTING = "bulk_product_routing" + +/** Entity constant for the bulk export job (product master + routing). */ +export const ENTITY_BULK_PRODUCT_ROUTING_EXPORT = "bulk_product_routing_export" + +/** Row-level error from a single sheet during bulk validation. */ +export interface BulkRowError { + rowNumber: number + field: string + message: string +} + +/** Per-sheet result from ValidateBulkProductRoutingFile. */ +export interface BulkSheetValidationResult { + sheetName: string + totalRows: number + errorCount: number + warningCount: number + sampleErrors: BulkRowError[] +} + +/** Full result from ValidateBulkProductRoutingFile. */ +export interface BulkValidationResult { + isValid: boolean + sheets: BulkSheetValidationResult[] +} From 65d6b1c2eb73539d52db30a025adf296e0838dcf Mon Sep 17 00:00:00 2001 From: ilhom Date: Mon, 22 Jun 2026 16:31:25 +0700 Subject: [PATCH 3/9] fix(frontend): fix Fragment key warning and isValid guard in BulkImportDialog - Import React to use React.Fragment with key prop on .map() (line 193) - Moved key from TableRow to Fragment to fix React key warning - Add isValid validation check to "Start Import" button (line 280) Co-Authored-By: Claude Sonnet 4.6 --- src/components/finance/costing/bulk-import-dialog.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/finance/costing/bulk-import-dialog.tsx b/src/components/finance/costing/bulk-import-dialog.tsx index 8ccc73d..5fc8c8f 100644 --- a/src/components/finance/costing/bulk-import-dialog.tsx +++ b/src/components/finance/costing/bulk-import-dialog.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useRef, useState } from "react" +import React, { useEffect, useRef, useState } from "react" import { AlertCircle, CheckCircle2, ChevronDown, ChevronRight, FileSpreadsheet, Loader2, Upload } from "lucide-react" import { toast } from "sonner" @@ -191,9 +191,8 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) {validation.sheets.map((sheet) => ( - <> + 0 ? "bg-destructive/5" : undefined} > {sheet.sheetName} @@ -245,7 +244,7 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) ))} - + ))} @@ -277,7 +276,7 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) )} {/* Start Import button — shown only when validation passed */} - {step === "validated" && !hasErrors && ( + {step === "validated" && !hasErrors && (validation?.isValid ?? true) && ( + {/* Import dropdown */} + + + + + + setImportOpen(true)}> + Import Produk + + setBulkImportOpen(true)}> + Import Produk + Routing (Bulk) + + + + + {/* Export dropdown */} + + + + + + + Export Produk + + void handleBulkExport()}> + Export Produk + Routing + + + + @@ -151,6 +208,14 @@ export default function ProductMasterPageClient() { /> )} + + queryClient.invalidateQueries({ queryKey: costProductMasterKeys.all }) + } + /> (null) const [expandedSheets, setExpandedSheets] = useState>(new Set()) const [jobId, setJobId] = useState(null) + const [templateLoading, setTemplateLoading] = useState(false) // Stable reset function — called from handleClose and when open changes function resetState() { @@ -65,6 +77,17 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) setStep("upload") } + async function handleDownloadTemplate() { + setTemplateLoading(true) + try { + await downloadBulkProductRoutingTemplate() + } catch (e) { + toast.error(`Template download failed: ${String(e)}`) + } finally { + setTemplateLoading(false) + } + } + async function handleValidate() { if (!file) return setStep("validating") @@ -113,6 +136,7 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) const isValidating = step === "validating" const isSubmitting = step === "submitting" const isDone = step === "done" + const canImport = step === "validated" && !hasErrors && (validation?.isValid ?? false) return ( @@ -122,39 +146,73 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps)
- {/* Step 1 — File upload */} -
-
fileRef.current?.click()} - > - - {file ? ( -
- - {file.name} - - ({(file.size / 1024).toFixed(1)} KB) - -
- ) : ( - <> - + {/* Template Download — hidden once job submitted */} + {!isDone && ( +
+
+ +
+

Import Template

- Click to select an Excel file (.xlsx) + Download the Excel template with required sheets

- - )} +
+
+ +
+ )} + + {/* File Upload — hidden once job submitted */} + {!isDone && ( +
+ +
fileRef.current?.click()} + > + + {file ? ( +
+ + {file.name} + + ({(file.size / 1024).toFixed(1)} KB) + +
+ ) : ( + <> + +

+ Click to select or drag and drop an Excel file +

+

+ Supported: .xlsx +

+ + )} +
-
+ )} - {/* Step 2 — Validation loading */} + {/* Validating spinner */} {isValidating && (
@@ -162,7 +220,7 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps)
)} - {/* Step 2 — Validation results */} + {/* Validation results table */} {validation && !isValidating && (
@@ -252,7 +310,7 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps)
)} - {/* Step 3 — Done */} + {/* Done */} {isDone && jobId !== null && (
@@ -276,7 +334,7 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) )} {/* Start Import button — shown only when validation passed */} - {step === "validated" && !hasErrors && (validation?.isValid ?? true) && ( + {canImport && ( + ) : ( + + )} + + + ) +} + +export function ImportJobsPageClient() { + const [entityFilter, setEntityFilter] = useState(ALL) + const [statusFilter, setStatusFilter] = useState(ALL) + const [page, setPage] = useState(1) + const pageSize = 20 + + const { data, isLoading, refetch, isFetching } = useImportJobs( + { + entity: entityFilter === ALL ? "" : entityFilter, + status: statusFilter === ALL ? "" : statusFilter, + page, + pageSize, + }, + 5000, + ) + + const items = data?.items ?? [] + + return ( +
+ + + + +
+ + + +
+ +
+ + + + Job ID + Tipe + Status + Progress + Dibuat + Selesai + File + + + + {isLoading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 7 }).map((__, j) => ( + +
+ + ))} + + )) + ) : items.length === 0 ? ( + + + + + + ) : ( + items.map((job) => ) + )} + +
+
+ + {(data?.totalItems ?? 0) > pageSize && ( + {}} + /> + )} +
+ ) +} diff --git a/src/app/(dashboard)/finance/import-jobs/loading.tsx b/src/app/(dashboard)/finance/import-jobs/loading.tsx new file mode 100644 index 0000000..1bb802c --- /dev/null +++ b/src/app/(dashboard)/finance/import-jobs/loading.tsx @@ -0,0 +1,13 @@ +import { TableSkeleton } from "@/components/loading" + +export default function ImportJobsLoading() { + return ( +
+
+
+
+
+ +
+ ) +} diff --git a/src/app/(dashboard)/finance/import-jobs/page.tsx b/src/app/(dashboard)/finance/import-jobs/page.tsx new file mode 100644 index 0000000..d97557a --- /dev/null +++ b/src/app/(dashboard)/finance/import-jobs/page.tsx @@ -0,0 +1,7 @@ +import { ImportJobsPageClient } from "./import-jobs-page-client" + +export const metadata = { title: "Import / Export Jobs" } + +export default function ImportJobsPage() { + return +} diff --git a/src/app/api/v1/finance/costing/import-jobs/route.ts b/src/app/api/v1/finance/costing/import-jobs/route.ts new file mode 100644 index 0000000..23208fe --- /dev/null +++ b/src/app/api/v1/finance/costing/import-jobs/route.ts @@ -0,0 +1,48 @@ +// GET /api/v1/finance/costing/import-jobs — list async import/export jobs + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const metadata = createMetadataFromRequest(request) + + const res = await getCostDataImportClient().listCostImportJobs( + { + entity: searchParams.get("entity") ?? "", + status: searchParams.get("status") ?? "", + pagination: { + page: Number(searchParams.get("page")) || 1, + pageSize: Number(searchParams.get("pageSize")) || 20, + }, + }, + metadata, + ) + + return NextResponse.json({ + base: res.base, + data: res.data, + pagination: res.pagination, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error listing import jobs:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to list import jobs", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/hooks/finance/use-cost-import.ts b/src/hooks/finance/use-cost-import.ts index b0fed16..d3ce8fc 100644 --- a/src/hooks/finance/use-cost-import.ts +++ b/src/hooks/finance/use-cost-import.ts @@ -10,15 +10,28 @@ import { importSync, importAsync, getImportJob, + listImportJobs, } from "@/services/finance/cost-import-api" import type { CostImportJob, ImportEntity, SyncImportResult, } from "@/types/finance/cost-import" +import type { ListImportJobsParams } from "@/services/finance/cost-import-api" export const costImportKeys = { job: (id: number) => ["finance", "cost-import", "job", id] as const, + jobs: (params?: ListImportJobsParams) => + ["finance", "cost-import", "jobs", params] as const, +} + +export function useImportJobs(params?: ListImportJobsParams, refetchIntervalMs?: number) { + return useQuery({ + queryKey: costImportKeys.jobs(params), + queryFn: () => listImportJobs(params), + staleTime: 5000, + refetchInterval: refetchIntervalMs, + }) } export function useDownloadTemplate() { diff --git a/src/services/finance/cost-import-api.ts b/src/services/finance/cost-import-api.ts index 77b0f7b..7472e96 100644 --- a/src/services/finance/cost-import-api.ts +++ b/src/services/finance/cost-import-api.ts @@ -106,6 +106,45 @@ export async function getImportJob(jobId: number): Promise { return json.data as CostImportJob } +export interface ListImportJobsParams { + entity?: string + status?: string + page?: number + pageSize?: number +} + +export interface ListImportJobsResult { + items: CostImportJob[] + totalItems: number + totalPages: number + currentPage: number + pageSize: number +} + +/** + * List import/export jobs with optional entity/status filters. + */ +export async function listImportJobs( + params?: ListImportJobsParams, +): Promise { + const qs = new URLSearchParams() + if (params?.entity) qs.set("entity", params.entity) + if (params?.status) qs.set("status", params.status) + if (params?.page) qs.set("page", String(params.page)) + if (params?.pageSize) qs.set("pageSize", String(params.pageSize)) + const res = await fetch(`${BASE}/import-jobs?${qs.toString()}`) + if (!res.ok) throw new Error(`List jobs failed: ${res.status}`) + const json = await res.json() + const raw: unknown[] = Array.isArray(json.data) ? json.data : [] + return { + items: raw as CostImportJob[], + totalItems: Number(json.pagination?.totalItems ?? raw.length), + totalPages: Number(json.pagination?.totalPages ?? 1), + currentPage: Number(json.pagination?.currentPage ?? 1), + pageSize: Number(json.pagination?.pageSize ?? 20), + } +} + // ── Bulk Product Routing ──────────────────────────────────────────────────── /** diff --git a/src/types/finance/cost-import.ts b/src/types/finance/cost-import.ts index b50c935..38b92f5 100644 --- a/src/types/finance/cost-import.ts +++ b/src/types/finance/cost-import.ts @@ -8,6 +8,8 @@ export type ImportEntity = | "product_master" | "capp" | "cpp" + | "bulk_product_routing" + | "bulk_product_routing_export" export interface CostImportJob { jobId: number From e0333a79fdf169ba24d8f4d9cf43ee772fe3562f Mon Sep 17 00:00:00 2001 From: ilhom Date: Mon, 22 Jun 2026 22:20:13 +0700 Subject: [PATCH 9/9] fix(bulk-import): make validation modal fully responsive on mobile - Replace DialogContent/Header/Footer with ScrollableDialog equivalents - Template card stacks vertically on mobile (flex-col sm:flex-row) - File name truncates with ellipsis (truncate min-w-0) - Validation table uses table-fixed to eliminate horizontal overflow - Error rows use break-all + pl-4 so full message is visible at 375px - Replace ScrollArea+overflow-x-auto (conflicting scroll containers) with a single div.max-h-64.overflow-auto wrapping the table Co-Authored-By: Claude Sonnet 4.6 --- .../finance/costing/bulk-import-dialog.tsx | 222 +++++++++--------- 1 file changed, 110 insertions(+), 112 deletions(-) diff --git a/src/components/finance/costing/bulk-import-dialog.tsx b/src/components/finance/costing/bulk-import-dialog.tsx index 734d9a9..7a0ef73 100644 --- a/src/components/finance/costing/bulk-import-dialog.tsx +++ b/src/components/finance/costing/bulk-import-dialog.tsx @@ -13,13 +13,7 @@ import { } from "lucide-react" import { toast } from "sonner" -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" +import { Dialog } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Label } from "@/components/ui/label" @@ -31,7 +25,13 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { ScrollArea } from "@/components/ui/scroll-area" +import { + ScrollableDialogContent, + ScrollableDialogHeader, + ScrollableDialogBody, + ScrollableDialogFooter, +} from "@/components/common/scrollable-dialog" +import { DialogTitle } from "@/components/ui/dialog" import { bulkImportProductMasterRouting, downloadBulkProductRoutingTemplate, @@ -140,27 +140,28 @@ export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) return ( - - + + Bulk Import — Product Master & Routing - + -
+ {/* Template Download — hidden once job submitted */} {!isDone && ( -
-
- -
+
+
+ +

Import Template

- Download the Excel template with required sheets + Download template Excel dengan 6 sheet yang diperlukan

- {/* Validate button — shown when file selected but not yet validated */} {file && step === "upload" && ( )} - {/* Start Import button — shown only when validation passed */} {canImport && ( )} - - + +
) }