From 8a4ca4365472cdf60fcdc26e6cc3574ce1a6b00b Mon Sep 17 00:00:00 2001 From: ilhom Date: Thu, 18 Jun 2026 14:30:08 +0700 Subject: [PATCH 01/22] =?UTF-8?q?chore(gen):=20regenerate=20proto=20TS=20?= =?UTF-8?q?=E2=80=94=20add=20YarnLookupFillService=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/generated/finance/v1/parameter.ts | 61 +- src/types/generated/finance/v1/yarn_master.ts | 557 +++++++++++++++++- 2 files changed, 616 insertions(+), 2 deletions(-) diff --git a/src/types/generated/finance/v1/parameter.ts b/src/types/generated/finance/v1/parameter.ts index ca042c3..78d95e0 100644 --- a/src/types/generated/finance/v1/parameter.ts +++ b/src/types/generated/finance/v1/parameter.ts @@ -155,6 +155,8 @@ export interface Parameter { displayOrder: number; /** Form section: Spec / Machine / Grade / Packing / Cost / etc. */ displayGroup: string; + /** Descriptive notes or formula hint for this parameter (read-only, populated from seed/migration). */ + notes: string; } /** CreateParameterRequest is the request for creating a new parameter. */ @@ -192,6 +194,8 @@ export interface CreateParameterRequest { displayOrder: number; /** Display group (optional, max 50 chars). E.g. 'Spec', 'Machine'. */ displayGroup: string; + /** Descriptive notes or formula hint (optional, max 500 chars). */ + notes: string; } /** CreateParameterResponse is the response for creating a parameter. */ @@ -284,7 +288,11 @@ export interface UpdateParameterRequest { | number | undefined; /** New display group (optional). Set to empty string to clear. */ - displayGroup?: string | undefined; + displayGroup?: + | string + | undefined; + /** New notes (optional). Set to empty string to clear. */ + notes?: string | undefined; } /** UpdateParameterResponse is the response for updating a parameter. */ @@ -439,6 +447,7 @@ function createBaseParameter(): Parameter { lookupMasterCode: "", displayOrder: 0, displayGroup: "", + notes: "", }; } @@ -504,6 +513,9 @@ export const Parameter: MessageFns = { if (message.displayGroup !== "") { writer.uint32(234).string(message.displayGroup); } + if (message.notes !== "") { + writer.uint32(242).string(message.notes); + } return writer; }, @@ -674,6 +686,14 @@ export const Parameter: MessageFns = { message.displayGroup = reader.string(); continue; } + case 30: { + if (tag !== 242) { + break; + } + + message.notes = reader.string(); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -781,6 +801,7 @@ export const Parameter: MessageFns = { : isSet(object.display_group) ? globalThis.String(object.display_group) : "", + notes: isSet(object.notes) ? globalThis.String(object.notes) : "", }; }, @@ -846,6 +867,9 @@ export const Parameter: MessageFns = { if (message.displayGroup !== "") { obj.displayGroup = message.displayGroup; } + if (message.notes !== "") { + obj.notes = message.notes; + } return obj; }, @@ -876,6 +900,7 @@ export const Parameter: MessageFns = { message.lookupMasterCode = object.lookupMasterCode ?? ""; message.displayOrder = object.displayOrder ?? 0; message.displayGroup = object.displayGroup ?? ""; + message.notes = object.notes ?? ""; return message; }, }; @@ -897,6 +922,7 @@ function createBaseCreateParameterRequest(): CreateParameterRequest { lookupMasterCode: "", displayOrder: 0, displayGroup: "", + notes: "", }; } @@ -947,6 +973,9 @@ export const CreateParameterRequest: MessageFns = { if (message.displayGroup !== "") { writer.uint32(122).string(message.displayGroup); } + if (message.notes !== "") { + writer.uint32(130).string(message.notes); + } return writer; }, @@ -1077,6 +1106,14 @@ export const CreateParameterRequest: MessageFns = { message.displayGroup = reader.string(); continue; } + case 16: { + if (tag !== 130) { + break; + } + + message.notes = reader.string(); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -1163,6 +1200,7 @@ export const CreateParameterRequest: MessageFns = { : isSet(object.display_group) ? globalThis.String(object.display_group) : "", + notes: isSet(object.notes) ? globalThis.String(object.notes) : "", }; }, @@ -1213,6 +1251,9 @@ export const CreateParameterRequest: MessageFns = { if (message.displayGroup !== "") { obj.displayGroup = message.displayGroup; } + if (message.notes !== "") { + obj.notes = message.notes; + } return obj; }, @@ -1236,6 +1277,7 @@ export const CreateParameterRequest: MessageFns = { message.lookupMasterCode = object.lookupMasterCode ?? ""; message.displayOrder = object.displayOrder ?? 0; message.displayGroup = object.displayGroup ?? ""; + message.notes = object.notes ?? ""; return message; }, }; @@ -1478,6 +1520,7 @@ function createBaseUpdateParameterRequest(): UpdateParameterRequest { lookupMasterCode: undefined, displayOrder: undefined, displayGroup: undefined, + notes: undefined, }; } @@ -1531,6 +1574,9 @@ export const UpdateParameterRequest: MessageFns = { if (message.displayGroup !== undefined) { writer.uint32(130).string(message.displayGroup); } + if (message.notes !== undefined) { + writer.uint32(138).string(message.notes); + } return writer; }, @@ -1669,6 +1715,14 @@ export const UpdateParameterRequest: MessageFns = { message.displayGroup = reader.string(); continue; } + case 17: { + if (tag !== 138) { + break; + } + + message.notes = reader.string(); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -1760,6 +1814,7 @@ export const UpdateParameterRequest: MessageFns = { : isSet(object.display_group) ? globalThis.String(object.display_group) : undefined, + notes: isSet(object.notes) ? globalThis.String(object.notes) : undefined, }; }, @@ -1813,6 +1868,9 @@ export const UpdateParameterRequest: MessageFns = { if (message.displayGroup !== undefined) { obj.displayGroup = message.displayGroup; } + if (message.notes !== undefined) { + obj.notes = message.notes; + } return obj; }, @@ -1837,6 +1895,7 @@ export const UpdateParameterRequest: MessageFns = { message.lookupMasterCode = object.lookupMasterCode ?? undefined; message.displayOrder = object.displayOrder ?? undefined; message.displayGroup = object.displayGroup ?? undefined; + message.notes = object.notes ?? undefined; return message; }, }; diff --git a/src/types/generated/finance/v1/yarn_master.ts b/src/types/generated/finance/v1/yarn_master.ts index 04a92e6..41e9ffe 100644 --- a/src/types/generated/finance/v1/yarn_master.ts +++ b/src/types/generated/finance/v1/yarn_master.ts @@ -1350,7 +1350,7 @@ export interface DeleteMBSpinResponse { /** ListMBSpinsRequest is the request for listing MB Spin records under a head. */ export interface ListMBSpinsRequest { - /** Parent MB Head UUID. */ + /** Parent MB Head UUID. Optional — omit or send empty to list all spins. */ mbhId: string; /** Page number (≥ 1). */ page: number; @@ -1442,6 +1442,45 @@ export interface DownloadMBSpinTemplateResponse { fileName: string; } +/** GetLookupFillValuesRequest requests auto-fill values from a master lookup selection. */ +export interface GetLookupFillValuesRequest { + /** + * lookup_master_code from mst_parameter.lookup_master_code. + * Valid values: "MACHINE" | "INTERMINGLING" | "PRODUCT_GRADE" | "MB_HEAD" | "BOX_BOBBIN_COST" + */ + lookupMasterCode: string; + /** Key of the selected master record (e.g., mc_code, intm_code, pg_code, mbh_mb_costing, bbc_code). */ + selectedKey: string; + /** + * Source param code that triggered the lookup — used to resolve CAP vs DEL variant for BOX_BOBBIN_COST. + * Optional; required only for BOX_BOBBIN_COST. + */ + sourceParamCode: string; +} + +/** GetLookupFillValuesResponse returns numeric and text fills to auto-populate CAPP params. */ +export interface GetLookupFillValuesResponse { + base: + | BaseResponse + | undefined; + /** param_code → numeric value to fill (e.g., {"MC_SPEED": 800.0, "MC_EFFICIENCY": 92.0}). */ + numericFills: { [key: string]: number }; + /** param_code → text value to fill (e.g., {"MB_DYE_NAME": "CIBA RED"}). */ + textFills: { [key: string]: string }; + /** Human-readable label for UI toast (e.g., "Barmag DTY Line D — 504 pos, 800 m/min"). */ + displayLabel: string; +} + +export interface GetLookupFillValuesResponse_NumericFillsEntry { + key: string; + value: number; +} + +export interface GetLookupFillValuesResponse_TextFillsEntry { + key: string; + value: string; +} + function createBaseMachine(): Machine { return { machineId: "", @@ -13329,6 +13368,452 @@ export const DownloadMBSpinTemplateResponse: MessageFns = { + encode(message: GetLookupFillValuesRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.lookupMasterCode !== "") { + writer.uint32(10).string(message.lookupMasterCode); + } + if (message.selectedKey !== "") { + writer.uint32(18).string(message.selectedKey); + } + if (message.sourceParamCode !== "") { + writer.uint32(26).string(message.sourceParamCode); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetLookupFillValuesRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetLookupFillValuesRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.lookupMasterCode = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.selectedKey = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.sourceParamCode = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetLookupFillValuesRequest { + return { + lookupMasterCode: isSet(object.lookupMasterCode) + ? globalThis.String(object.lookupMasterCode) + : isSet(object.lookup_master_code) + ? globalThis.String(object.lookup_master_code) + : "", + selectedKey: isSet(object.selectedKey) + ? globalThis.String(object.selectedKey) + : isSet(object.selected_key) + ? globalThis.String(object.selected_key) + : "", + sourceParamCode: isSet(object.sourceParamCode) + ? globalThis.String(object.sourceParamCode) + : isSet(object.source_param_code) + ? globalThis.String(object.source_param_code) + : "", + }; + }, + + toJSON(message: GetLookupFillValuesRequest): unknown { + const obj: any = {}; + if (message.lookupMasterCode !== "") { + obj.lookupMasterCode = message.lookupMasterCode; + } + if (message.selectedKey !== "") { + obj.selectedKey = message.selectedKey; + } + if (message.sourceParamCode !== "") { + obj.sourceParamCode = message.sourceParamCode; + } + return obj; + }, + + create(base?: DeepPartial): GetLookupFillValuesRequest { + return GetLookupFillValuesRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetLookupFillValuesRequest { + const message = createBaseGetLookupFillValuesRequest(); + message.lookupMasterCode = object.lookupMasterCode ?? ""; + message.selectedKey = object.selectedKey ?? ""; + message.sourceParamCode = object.sourceParamCode ?? ""; + return message; + }, +}; + +function createBaseGetLookupFillValuesResponse(): GetLookupFillValuesResponse { + return { base: undefined, numericFills: {}, textFills: {}, displayLabel: "" }; +} + +export const GetLookupFillValuesResponse: MessageFns = { + encode(message: GetLookupFillValuesResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + globalThis.Object.entries(message.numericFills).forEach(([key, value]: [string, number]) => { + GetLookupFillValuesResponse_NumericFillsEntry.encode({ key: key as any, value }, writer.uint32(18).fork()).join(); + }); + globalThis.Object.entries(message.textFills).forEach(([key, value]: [string, string]) => { + GetLookupFillValuesResponse_TextFillsEntry.encode({ key: key as any, value }, writer.uint32(26).fork()).join(); + }); + if (message.displayLabel !== "") { + writer.uint32(34).string(message.displayLabel); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetLookupFillValuesResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetLookupFillValuesResponse(); + 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 !== 18) { + break; + } + + const entry2 = GetLookupFillValuesResponse_NumericFillsEntry.decode(reader, reader.uint32()); + if (entry2.value !== undefined) { + message.numericFills[entry2.key] = entry2.value; + } + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + const entry3 = GetLookupFillValuesResponse_TextFillsEntry.decode(reader, reader.uint32()); + if (entry3.value !== undefined) { + message.textFills[entry3.key] = entry3.value; + } + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.displayLabel = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetLookupFillValuesResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + numericFills: isObject(object.numericFills) + ? (globalThis.Object.entries(object.numericFills) as [string, any][]).reduce( + (acc: { [key: string]: number }, [key, value]: [string, any]) => { + acc[key] = globalThis.Number(value); + return acc; + }, + {}, + ) + : isObject(object.numeric_fills) + ? (globalThis.Object.entries(object.numeric_fills) as [string, any][]).reduce( + (acc: { [key: string]: number }, [key, value]: [string, any]) => { + acc[key] = globalThis.Number(value); + return acc; + }, + {}, + ) + : {}, + textFills: isObject(object.textFills) + ? (globalThis.Object.entries(object.textFills) as [string, any][]).reduce( + (acc: { [key: string]: string }, [key, value]: [string, any]) => { + acc[key] = globalThis.String(value); + return acc; + }, + {}, + ) + : isObject(object.text_fills) + ? (globalThis.Object.entries(object.text_fills) as [string, any][]).reduce( + (acc: { [key: string]: string }, [key, value]: [string, any]) => { + acc[key] = globalThis.String(value); + return acc; + }, + {}, + ) + : {}, + displayLabel: isSet(object.displayLabel) + ? globalThis.String(object.displayLabel) + : isSet(object.display_label) + ? globalThis.String(object.display_label) + : "", + }; + }, + + toJSON(message: GetLookupFillValuesResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.numericFills) { + const entries = globalThis.Object.entries(message.numericFills) as [string, number][]; + if (entries.length > 0) { + obj.numericFills = {}; + entries.forEach(([k, v]) => { + obj.numericFills[k] = v; + }); + } + } + if (message.textFills) { + const entries = globalThis.Object.entries(message.textFills) as [string, string][]; + if (entries.length > 0) { + obj.textFills = {}; + entries.forEach(([k, v]) => { + obj.textFills[k] = v; + }); + } + } + if (message.displayLabel !== "") { + obj.displayLabel = message.displayLabel; + } + return obj; + }, + + create(base?: DeepPartial): GetLookupFillValuesResponse { + return GetLookupFillValuesResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GetLookupFillValuesResponse { + const message = createBaseGetLookupFillValuesResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.numericFills = (globalThis.Object.entries(object.numericFills ?? {}) as [string, number][]).reduce( + (acc: { [key: string]: number }, [key, value]: [string, number]) => { + if (value !== undefined) { + acc[key] = globalThis.Number(value); + } + return acc; + }, + {}, + ); + message.textFills = (globalThis.Object.entries(object.textFills ?? {}) as [string, string][]).reduce( + (acc: { [key: string]: string }, [key, value]: [string, string]) => { + if (value !== undefined) { + acc[key] = globalThis.String(value); + } + return acc; + }, + {}, + ); + message.displayLabel = object.displayLabel ?? ""; + return message; + }, +}; + +function createBaseGetLookupFillValuesResponse_NumericFillsEntry(): GetLookupFillValuesResponse_NumericFillsEntry { + return { key: "", value: 0 }; +} + +export const GetLookupFillValuesResponse_NumericFillsEntry: MessageFns = + { + encode( + message: GetLookupFillValuesResponse_NumericFillsEntry, + writer: BinaryWriter = new BinaryWriter(), + ): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== 0) { + writer.uint32(17).double(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetLookupFillValuesResponse_NumericFillsEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetLookupFillValuesResponse_NumericFillsEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.value = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetLookupFillValuesResponse_NumericFillsEntry { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? globalThis.Number(object.value) : 0, + }; + }, + + toJSON(message: GetLookupFillValuesResponse_NumericFillsEntry): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== 0) { + obj.value = message.value; + } + return obj; + }, + + create( + base?: DeepPartial, + ): GetLookupFillValuesResponse_NumericFillsEntry { + return GetLookupFillValuesResponse_NumericFillsEntry.fromPartial(base ?? {}); + }, + fromPartial( + object: DeepPartial, + ): GetLookupFillValuesResponse_NumericFillsEntry { + const message = createBaseGetLookupFillValuesResponse_NumericFillsEntry(); + message.key = object.key ?? ""; + message.value = object.value ?? 0; + return message; + }, + }; + +function createBaseGetLookupFillValuesResponse_TextFillsEntry(): GetLookupFillValuesResponse_TextFillsEntry { + return { key: "", value: "" }; +} + +export const GetLookupFillValuesResponse_TextFillsEntry: MessageFns = { + encode(message: GetLookupFillValuesResponse_TextFillsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetLookupFillValuesResponse_TextFillsEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetLookupFillValuesResponse_TextFillsEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetLookupFillValuesResponse_TextFillsEntry { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? globalThis.String(object.value) : "", + }; + }, + + toJSON(message: GetLookupFillValuesResponse_TextFillsEntry): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== "") { + obj.value = message.value; + } + return obj; + }, + + create(base?: DeepPartial): GetLookupFillValuesResponse_TextFillsEntry { + return GetLookupFillValuesResponse_TextFillsEntry.fromPartial(base ?? {}); + }, + fromPartial( + object: DeepPartial, + ): GetLookupFillValuesResponse_TextFillsEntry { + const message = createBaseGetLookupFillValuesResponse_TextFillsEntry(); + message.key = object.key ?? ""; + message.value = object.value ?? ""; + return message; + }, +}; + /** MachineService manages yarn machine master data. */ export type MachineServiceDefinition = typeof MachineServiceDefinition; export const MachineServiceDefinition = { @@ -16238,6 +16723,72 @@ export const MBSpinServiceDefinition = { }, } as const; +/** + * YarnLookupFillService provides a unified "fill from master" API used by the CAPP form. + * Called when the user selects a lookup param value (e.g., a machine code). + * Returns numeric and text fills to auto-populate related CAPP params. + */ +export type YarnLookupFillServiceDefinition = typeof YarnLookupFillServiceDefinition; +export const YarnLookupFillServiceDefinition = { + name: "YarnLookupFillService", + fullName: "finance.v1.YarnLookupFillService", + methods: { + /** GetLookupFillValues returns numeric and text fills to auto-populate CAPP params. */ + getLookupFillValues: { + name: "GetLookupFillValues", + requestType: GetLookupFillValuesRequest, + requestStream: false, + responseType: GetLookupFillValuesResponse, + responseStream: false, + options: { + _unknownFields: { + 578365826: [ + new Uint8Array([ + 36, + 18, + 34, + 47, + 97, + 112, + 105, + 47, + 118, + 49, + 47, + 102, + 105, + 110, + 97, + 110, + 99, + 101, + 47, + 108, + 111, + 111, + 107, + 117, + 112, + 45, + 102, + 105, + 108, + 108, + 45, + 118, + 97, + 108, + 117, + 101, + 115, + ]), + ], + }, + }, + }, + }, +} as const; + function bytesFromBase64(b64: string): Uint8Array { if ((globalThis as any).Buffer) { return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); @@ -16271,6 +16822,10 @@ export type DeepPartial = T extends Builtin ? T : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; +function isObject(value: any): boolean { + return typeof value === "object" && value !== null; +} + function isSet(value: any): boolean { return value !== null && value !== undefined; } From ff0547787a740a18466bc5856a0dca85962c896a Mon Sep 17 00:00:00 2001 From: ilhom Date: Thu, 18 Jun 2026 14:39:25 +0700 Subject: [PATCH 02/22] feat(frontend): add lookup-fill-values BFF route and gRPC client - Add YarnLookupFillServiceDefinition to yarn_master import in clients.ts - Add getYarnLookupFillClient() singleton using getOrCreate pattern - Export getYarnLookupFillClient from grpc/index.ts barrel - Create BFF route GET /api/v1/finance/lookup-fill-values with validation - Add src/types/finance/yarn-master.ts with LookupFillValuesResponse and LOOKUP_MASTER_CONFIG Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../v1/finance/lookup-fill-values/route.ts | 40 +++++++++++++++++++ src/lib/grpc/clients.ts | 7 ++++ src/lib/grpc/index.ts | 1 + src/types/finance/yarn-master.ts | 25 ++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 src/app/api/v1/finance/lookup-fill-values/route.ts create mode 100644 src/types/finance/yarn-master.ts diff --git a/src/app/api/v1/finance/lookup-fill-values/route.ts b/src/app/api/v1/finance/lookup-fill-values/route.ts new file mode 100644 index 0000000..2af0bb6 --- /dev/null +++ b/src/app/api/v1/finance/lookup-fill-values/route.ts @@ -0,0 +1,40 @@ +import { type NextRequest, NextResponse } from "next/server" +import { createMetadataFromRequest, isGrpcError, handleGrpcError } from "@/lib/grpc" +import { getYarnLookupFillClient } from "@/lib/grpc/clients" + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const lookupMasterCode = searchParams.get("lookupMasterCode") ?? "" + const selectedKey = searchParams.get("selectedKey") ?? "" + const sourceParamCode = searchParams.get("sourceParamCode") ?? "" + + if (!lookupMasterCode || !selectedKey) { + return NextResponse.json( + { base: { isSuccess: false, message: "lookupMasterCode and selectedKey are required" } }, + { status: 400 } + ) + } + + const metadata = createMetadataFromRequest(request) + const response = await getYarnLookupFillClient().getLookupFillValues( + { lookupMasterCode, selectedKey, sourceParamCode }, + metadata + ) + + return NextResponse.json({ + base: response.base, + data: { + numericFills: response.numericFills, + textFills: response.textFills, + displayLabel: response.displayLabel, + }, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + return NextResponse.json( + { base: { isSuccess: false, message: "Internal error" } }, + { status: 500 } + ) + } +} diff --git a/src/lib/grpc/clients.ts b/src/lib/grpc/clients.ts index a88eb78..0ae35d0 100644 --- a/src/lib/grpc/clients.ts +++ b/src/lib/grpc/clients.ts @@ -72,6 +72,7 @@ import { MBSpinServiceDefinition, MBHeadServiceDefinition, ProductGradeServiceDefinition, + YarnLookupFillServiceDefinition, } from "@/types/generated/finance/v1/yarn_master" const CHANNEL_OPTIONS = { @@ -463,3 +464,9 @@ export function getMBHeadClient() { createServiceClient(MBHeadServiceDefinition, SERVICE_ADDRESSES.finance, insecure, CHANNEL_OPTIONS) ) } + +export function getYarnLookupFillClient() { + return getOrCreate("yarnLookupFill", () => + createServiceClient(YarnLookupFillServiceDefinition, SERVICE_ADDRESSES.finance, insecure, CHANNEL_OPTIONS) + ) +} diff --git a/src/lib/grpc/index.ts b/src/lib/grpc/index.ts index 56a4d07..12f4e3d 100644 --- a/src/lib/grpc/index.ts +++ b/src/lib/grpc/index.ts @@ -60,4 +60,5 @@ export { getMBSpinClient, getProductGradeClient, getMBHeadClient, + getYarnLookupFillClient, } from "./clients" diff --git a/src/types/finance/yarn-master.ts b/src/types/finance/yarn-master.ts new file mode 100644 index 0000000..945cb5b --- /dev/null +++ b/src/types/finance/yarn-master.ts @@ -0,0 +1,25 @@ +export interface LookupFillValuesResponse { + numericFills: Record + textFills: Record + displayLabel: string +} + +// Map lookup_master_code → BFF list endpoint + field mapping +export const LOOKUP_MASTER_CONFIG: Record< + string, + { + apiPath: string + codeField: string + labelField: string + } +> = { + MACHINE: { apiPath: "/api/v1/finance/machines", codeField: "machineCode", labelField: "machineName" }, + INTERMINGLING: { + apiPath: "/api/v1/finance/interminglings", + codeField: "interminglingCode", + labelField: "interminglingCode", + }, + PRODUCT_GRADE: { apiPath: "/api/v1/finance/product-grades", codeField: "pgCode", labelField: "pgName" }, + MB_HEAD: { apiPath: "/api/v1/finance/mb-heads", codeField: "mbhMbCosting", labelField: "mbhMgtName" }, + BOX_BOBBIN_COST: { apiPath: "/api/v1/finance/box-bobbin-costs", codeField: "bbcCode", labelField: "bbcName" }, +} From afae6e0eef0b7cc847e2307b8b915ce72076972d Mon Sep 17 00:00:00 2001 From: ilhom Date: Thu, 18 Jun 2026 14:44:02 +0700 Subject: [PATCH 03/22] feat(frontend): add MasterLookupField component and use-master-lookup hook Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../master-lookup-field.tsx | 144 ++++++++++++++++++ src/hooks/finance/use-master-lookup.ts | 36 +++++ 2 files changed, 180 insertions(+) create mode 100644 src/components/finance/cost-product-master/master-lookup-field.tsx create mode 100644 src/hooks/finance/use-master-lookup.ts diff --git a/src/components/finance/cost-product-master/master-lookup-field.tsx b/src/components/finance/cost-product-master/master-lookup-field.tsx new file mode 100644 index 0000000..fc21363 --- /dev/null +++ b/src/components/finance/cost-product-master/master-lookup-field.tsx @@ -0,0 +1,144 @@ +"use client" + +import { useState, useCallback } from "react" +import { Check, ChevronsUpDown, Loader2 } from "lucide-react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { useMasterLookupOptions } from "@/hooks/finance/use-master-lookup" +import type { LookupFillValuesResponse } from "@/types/finance/yarn-master" +import type { RequiredParamEntry } from "@/types/finance/cost-product-parameter" + +interface DraftValue { + valueNumeric: string + valueText: string + valueFlag: boolean + hasValueFlag: boolean + dirty: boolean +} + +interface MasterLookupFieldProps { + entry: RequiredParamEntry + draft: DraftValue + allEntries: RequiredParamEntry[] + onChangeLookup: ( + triggerParamId: string, + selectedKey: string, + fills: LookupFillValuesResponse | null + ) => void +} + +export function MasterLookupField({ + entry, + draft, + allEntries: _allEntries, + onChangeLookup, +}: MasterLookupFieldProps) { + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + const { data: options = [], isLoading: optionsLoading } = useMasterLookupOptions( + entry.lookupMasterCode + ) + + const currentValue = draft.valueText + + const handleSelect = useCallback( + async (selectedKey: string) => { + setOpen(false) + if (selectedKey === currentValue) return + + setLoading(true) + try { + const params = new URLSearchParams({ + lookupMasterCode: entry.lookupMasterCode ?? "", + selectedKey, + sourceParamCode: entry.paramCode, + }) + const res = await fetch(`/api/v1/finance/lookup-fill-values?${params.toString()}`) + if (res.ok) { + const json = (await res.json()) as { data?: LookupFillValuesResponse } + onChangeLookup(entry.paramId, selectedKey, json.data ?? null) + } else { + onChangeLookup(entry.paramId, selectedKey, null) + } + } finally { + setLoading(false) + } + }, + [entry.paramId, entry.paramCode, entry.lookupMasterCode, currentValue, onChangeLookup] + ) + + const selectedOption = options.find((o) => o.value === currentValue) + const displayValue = selectedOption?.label ?? currentValue ?? "" + + return ( +
+ + + + + + + + + {optionsLoading && ( +
+ Loading… +
+ )} + No results found. + + {options.map((opt) => ( + + + {opt.label} + + ))} + +
+
+
+
+ {currentValue && ( +

+ Auto-fills related params on selection change. +

+ )} +
+ ) +} diff --git a/src/hooks/finance/use-master-lookup.ts b/src/hooks/finance/use-master-lookup.ts new file mode 100644 index 0000000..491e463 --- /dev/null +++ b/src/hooks/finance/use-master-lookup.ts @@ -0,0 +1,36 @@ +import { useQuery } from "@tanstack/react-query" +import { LOOKUP_MASTER_CONFIG } from "@/types/finance/yarn-master" + +interface MasterOption { + value: string // the code stored to CPP (e.g., machine code) + label: string // display text in dropdown +} + +async function fetchMasterOptions(lookupMasterCode: string): Promise { + const config = LOOKUP_MASTER_CONFIG[lookupMasterCode] + if (!config) return [] + + const res = await fetch(`${config.apiPath}?pageSize=500&activeFilter=1`) + if (!res.ok) return [] + const json = (await res.json()) as { + data?: { items?: Record[] } | Record[] + } + const items: Record[] = Array.isArray(json.data) + ? json.data + : ((json.data as { items?: Record[] })?.items ?? []) + return items + .map((item) => ({ + value: String(item[config.codeField] ?? ""), + label: String(item[config.labelField] ?? item[config.codeField] ?? ""), + })) + .filter((o) => o.value !== "") +} + +export function useMasterLookupOptions(lookupMasterCode: string | undefined) { + return useQuery({ + queryKey: ["finance", "master-lookup", "options", lookupMasterCode], + queryFn: () => fetchMasterOptions(lookupMasterCode!), + enabled: !!lookupMasterCode, + staleTime: 60_000, + }) +} From 0c166b91a661b3c6041b9428cd70b8a4a9749993 Mon Sep 17 00:00:00 2001 From: ilhom Date: Thu, 18 Jun 2026 14:47:10 +0700 Subject: [PATCH 04/22] fix(frontend): add _allEntries comment and guard CommandEmpty during loading - Add clarifying comment to MasterLookupFieldProps.allEntries: it is passed for context but auto-population happens in the parent via onChangeLookup - Wrap CommandEmpty in !optionsLoading guard to prevent flicker when options are still loading from the lookup master query Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../master-lookup-field.tsx | 3 +- .../parameter/parameter-form-dialog.tsx | 31 +++++++++++++++++++ src/types/finance/parameter.ts | 2 ++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/finance/cost-product-master/master-lookup-field.tsx b/src/components/finance/cost-product-master/master-lookup-field.tsx index fc21363..28c9772 100644 --- a/src/components/finance/cost-product-master/master-lookup-field.tsx +++ b/src/components/finance/cost-product-master/master-lookup-field.tsx @@ -28,6 +28,7 @@ interface DraftValue { interface MasterLookupFieldProps { entry: RequiredParamEntry draft: DraftValue + // allEntries is passed for context but auto-population happens in the parent via onChangeLookup. allEntries: RequiredParamEntry[] onChangeLookup: ( triggerParamId: string, @@ -116,7 +117,7 @@ export function MasterLookupField({ Loading… )} - No results found. + {!optionsLoading && No results found.} {options.map((opt) => ( diff --git a/src/components/finance/parameter/parameter-form-dialog.tsx b/src/components/finance/parameter/parameter-form-dialog.tsx index a588564..1d25237 100644 --- a/src/components/finance/parameter/parameter-form-dialog.tsx +++ b/src/components/finance/parameter/parameter-form-dialog.tsx @@ -25,6 +25,7 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, @@ -63,6 +64,7 @@ interface ParameterFormValues { lookupMasterCode: string displayOrder: number displayGroup: string + notes: string } const parameterFormSchema = z.object({ @@ -98,6 +100,7 @@ const parameterFormSchema = z.object({ lookupMasterCode: z.string().max(30), displayOrder: z.coerce.number().int().gte(0), displayGroup: z.string().max(50), + notes: z.string().max(500), }) interface ParameterFormDialogProps { @@ -147,6 +150,7 @@ export function ParameterFormDialog({ lookupMasterCode: "", displayOrder: 0, displayGroup: "", + notes: "", }, }) @@ -170,6 +174,7 @@ export function ParameterFormDialog({ lookupMasterCode: parameter.lookupMasterCode || "", displayOrder: parameter.displayOrder ?? 0, displayGroup: parameter.displayGroup || "", + notes: parameter.notes || "", }) } else { form.reset({ @@ -189,6 +194,7 @@ export function ParameterFormDialog({ lookupMasterCode: "", displayOrder: 0, displayGroup: "", + notes: "", }) } } @@ -216,6 +222,7 @@ export function ParameterFormDialog({ lookupMasterCode: values.lookupMasterCode, displayOrder: values.displayOrder, displayGroup: values.displayGroup, + notes: values.notes, }, }) } else { @@ -235,6 +242,7 @@ export function ParameterFormDialog({ lookupMasterCode: values.lookupMasterCode, displayOrder: values.displayOrder, displayGroup: values.displayGroup, + notes: values.notes, }) } onOpenChange(false) @@ -639,6 +647,29 @@ export function ParameterFormDialog({ + ( + + Notes + +