305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
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<TRow> = Extract<keyof TRow, string> | string
|
|
const scaleT = (key: string, params?: Record<string, unknown>) =>
|
|
params ? i18n.global.t(`pricingScale.${key}`, params) : i18n.global.t(`pricingScale.${key}`)
|
|
|
|
export const createScaleValueColumn = <TRow>(options: {
|
|
headerName: string
|
|
field: ScaleColumnField<TRow>
|
|
headerTooltip: string
|
|
onReset: () => Promise<void> | 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<TRow> => ({
|
|
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 = <TRow>(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<string, unknown>
|
|
}) : ColGroupDef<TRow> => ({
|
|
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 = <TRow>(options: {
|
|
headerComponent: any
|
|
restoreConsultCategoryFactorColumnDefaults: () => Promise<void> | void
|
|
restoreMajorFactorColumnDefaults: () => Promise<void> | void
|
|
parseNumberOrNull: (value: any, options?: any) => any
|
|
getBudgetFee: (row: TRow | undefined) => number | null
|
|
aggFunc: any
|
|
}) : ColGroupDef<TRow> => ({
|
|
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 = <TRow>() : ColDef<TRow> => ({
|
|
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 = <TRow>(options: {
|
|
totalLabel: string
|
|
idLabelMap: Map<string, string>
|
|
parseProjectIndexFromPathKey: (key: string) => number | null
|
|
}) : ColDef<TRow> => ({
|
|
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
|
|
}
|
|
})
|