import type { ColDef, ColGroupDef } from 'ag-grid-community' import { formatScaleEditableNumber, formatScaleReadonlyMoney, getScaleMergeColSpanBeforeTotal } from '@/lib/pricingScaleGrid' import { AgGridResetHeader } from '@/lib/agGridResetHeader' import { i18n } from '@/i18n' type ScaleColumnField = Extract | string const scaleT = (key: string, params?: Record) => params ? i18n.global.t(`pricingScale.${key}`, params) : i18n.global.t(`pricingScale.${key}`) export const createScaleValueColumn = (options: { headerName: string field: ScaleColumnField headerTooltip: string onReset: () => Promise | void resetTitle: string headerComponent: any minWidth?: number flex?: number isEditable: (row: TRow | undefined) => boolean emptyTextPredicate: (row: TRow | undefined, value: unknown) => boolean valueParser: (params: any) => any valueFormatter: (params: any) => string }) : ColDef => ({ headerName: options.headerName, field: options.field as any, headerTooltip: options.headerTooltip, headerComponent: options.headerComponent, headerComponentParams: { onReset: options.onReset, resetTitle: options.resetTitle }, headerClass: 'ag-right-aligned-header', minWidth: options.minWidth ?? 90, flex: options.flex ?? 2, editable: params => !params.node?.group && !params.node?.rowPinned && options.isEditable(params.data), cellClass: params => !params.node?.group && !params.node?.rowPinned && options.isEditable(params.data) ? 'editable-cell-line' : '', cellClassRules: { 'ag-right-aligned-cell': () => true, 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && options.emptyTextPredicate(params.data, params.value) }, valueParser: options.valueParser, valueFormatter: options.valueFormatter }) export const createScaleBenchmarkBudgetColumnGroup = (options: { getCheckedSplit: (row: TRow | undefined) => { basic?: number | null; optional?: number | null; total?: number | null } | null createBudgetCellRendererWithCheck: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => any getHeaderComponent?: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => any getHeaderComponentParams?: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => Record }) : ColGroupDef => ({ headerName: scaleT('columns.benchmarkBudget'), marryChildren: true, children: [ { headerName: scaleT('columns.basicWork'), field: 'benchmarkBudgetBasic' as any, colId: 'benchmarkBudgetBasic', headerClass: 'ag-right-aligned-header', headerComponent: options.getHeaderComponent?.('benchmarkBudgetBasicChecked'), headerComponentParams: options.getHeaderComponentParams?.('benchmarkBudgetBasicChecked'), minWidth: 130, flex: 1, cellClassRules: { 'ag-right-aligned-cell': () => true }, valueGetter: params => (params.node?.rowPinned ? null : options.getCheckedSplit(params.data)?.basic ?? null), cellRenderer: options.createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'), cellRendererParams: { suppressMouseEventHandling: () => true }, valueFormatter: formatScaleReadonlyMoney }, { headerName: scaleT('columns.optionalWork'), field: 'benchmarkBudgetOptional' as any, colId: 'benchmarkBudgetOptional', headerClass: 'ag-right-aligned-header', headerComponent: options.getHeaderComponent?.('benchmarkBudgetOptionalChecked'), headerComponentParams: options.getHeaderComponentParams?.('benchmarkBudgetOptionalChecked'), minWidth: 130, flex: 1, cellClassRules: { 'ag-right-aligned-cell': () => true }, valueGetter: params => (params.node?.rowPinned ? null : options.getCheckedSplit(params.data)?.optional ?? null), cellRenderer: options.createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'), cellRendererParams: { suppressMouseEventHandling: () => true }, valueFormatter: formatScaleReadonlyMoney }, { headerName: scaleT('columns.subtotal'), field: 'benchmarkBudget' as any, colId: 'benchmarkBudgetTotal', headerClass: 'ag-right-aligned-header', minWidth: 100, flex: 1, cellClassRules: { 'ag-right-aligned-cell': () => true }, valueGetter: params => (params.node?.rowPinned ? null : options.getCheckedSplit(params.data)?.total ?? null), valueFormatter: formatScaleReadonlyMoney } ] }) export const createScaleBudgetFeeColumnGroup = (options: { headerComponent: any restoreConsultCategoryFactorColumnDefaults: () => Promise | void restoreMajorFactorColumnDefaults: () => Promise | void parseNumberOrNull: (value: any, options?: any) => any getBudgetFee: (row: TRow | undefined) => number | null aggFunc: any }) : ColGroupDef => ({ headerName: scaleT('columns.budgetFee'), marryChildren: true, children: [ { headerName: scaleT('columns.consultCategoryFactor'), field: 'consultCategoryFactor' as any, colId: 'consultCategoryFactor', headerTooltip: scaleT('tooltip.resetConsultCategoryFactor'), headerComponent: options.headerComponent, headerComponentParams: { onReset: options.restoreConsultCategoryFactorColumnDefaults, resetTitle: scaleT('tooltip.resetConsultCategoryFactor') }, headerClass: 'ag-right-aligned-header', minWidth: 80, flex: 1, editable: params => !params.node?.group && !params.node?.rowPinned, cellClass: params => !params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : '', cellClassRules: { 'ag-right-aligned-cell': () => true, 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, valueParser: params => options.parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: params => formatScaleEditableNumber(params) }, { headerName: scaleT('columns.majorFactor'), field: 'majorFactor' as any, colId: 'majorFactor', headerTooltip: scaleT('tooltip.resetMajorFactor'), headerComponent: options.headerComponent, headerComponentParams: { onReset: options.restoreMajorFactorColumnDefaults, resetTitle: scaleT('tooltip.resetMajorFactor') }, headerClass: 'ag-right-aligned-header', minWidth: 80, flex: 1, editable: params => !params.node?.group && !params.node?.rowPinned, cellClass: params => !params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : '', cellClassRules: { 'ag-right-aligned-cell': () => true, 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, valueParser: params => options.parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: params => formatScaleEditableNumber(params) }, { headerName: scaleT('columns.workStageFactor'), field: 'workStageFactor' as any, colId: 'workStageFactor', headerClass: 'ag-right-aligned-header', minWidth: 80, flex: 1, editable: params => !params.node?.group && !params.node?.rowPinned, cellClass: params => !params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : '', cellClassRules: { 'ag-right-aligned-cell': () => true, 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, valueParser: params => options.parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: params => formatScaleEditableNumber(params) }, { headerName: scaleT('columns.workRatio'), field: 'workRatio' as any, colId: 'workRatio', headerTooltip: scaleT('tooltip.workRatio'), headerComponent: AgGridResetHeader, headerClass: 'ag-right-aligned-header', minWidth: 80, flex: 1, editable: params => !params.node?.group && !params.node?.rowPinned, cellClass: params => !params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : '', cellClassRules: { 'ag-right-aligned-cell': () => true, 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, valueParser: params => options.parseNumberOrNull(params.newValue, { precision: 2 }), valueFormatter: params => formatScaleEditableNumber(params, 2) }, { headerName: scaleT('columns.total'), field: 'budgetFee' as any, colId: 'budgetFeeTotal', headerClass: 'ag-right-aligned-header', minWidth: 120, flex: 1, aggFunc: options.aggFunc, cellClassRules: { 'ag-right-aligned-cell': () => true }, valueGetter: params => (params.node?.rowPinned ? (params.data as any)?.budgetFee ?? null : options.getBudgetFee(params.data)), valueFormatter: formatScaleReadonlyMoney } ] }) export const createScaleRemarkColumn = () : ColDef => ({ headerName: scaleT('columns.remark'), field: 'remark' as any, minWidth: 100, flex: 1.2, cellEditor: 'agLargeTextCellEditor', wrapText: true, autoHeight: true, cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' }, editable: params => !params.node?.group && !params.node?.rowPinned, valueFormatter: params => { if (!params.node?.group && !params.node?.rowPinned && !params.value) return scaleT('clickToInput') return params.value || '' }, cellClass: params => (!params.node?.group && !params.node?.rowPinned ? ' remark-wrap-cell' : ''), cellClassRules: { 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') } }) export const createScaleAutoGroupColumn = (options: { totalLabel: string idLabelMap: Map parseProjectIndexFromPathKey: (key: string) => number | null }) : ColDef => ({ headerName: scaleT('columns.majorGroup'), minWidth: 250, flex: 2, wrapText: true, autoHeight: true, cellClassRules: { 'ag-summary-label-cell': params => Boolean(params.node?.rowPinned) }, cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' }, cellRendererParams: { suppressCount: true }, colSpan: getScaleMergeColSpanBeforeTotal, valueFormatter: params => { if (params.node?.rowPinned) { return options.totalLabel } const rowData = params.data as any if (!params.node?.group && rowData?.majorCode && rowData?.majorName) { return `${rowData.majorCode} ${rowData.majorName}` } const nodeId = String(params.value || '') const projectIndex = options.parseProjectIndexFromPathKey(nodeId) if (projectIndex != null) return scaleT('projectLabel', { index: projectIndex }) return options.idLabelMap.get(nodeId) || nodeId }, tooltipValueGetter: params => { if (params.node?.rowPinned) return options.totalLabel const rowData = params.data as any if (!params.node?.group && rowData?.majorCode && rowData?.majorName) { return `${rowData.majorCode} ${rowData.majorName}` } const nodeId = String(params.value || '') const projectIndex = options.parseProjectIndexFromPathKey(nodeId) if (projectIndex != null) return scaleT('projectLabel', { index: projectIndex }) return options.idLabelMap.get(nodeId) || nodeId } })