table
const columns = useMemo<TableColumnsType<ProposalData>>(() => {
const visibleCols = PROPOSAL_COLUMNS.filter((col) => visibleColumnKeys.includes(col.key));
// 左固定の基準インデックス(visibleCols 上の位置)。左固定は静的列のみ対象。
// モーダル側で妥当性を検証済みのため、ここでは指定列の位置をそのまま採用する。
// const leftFixedCol = leftFixedKey && leftFixedKey.length > 0 ? PROPOSAL_COLUMNS.find((c) => c.key === leftFixedKey) : undefined;
// const leftIndex = leftFixedCol && !leftFixedCol.monthly ? visibleCols.findIndex((col) => col.key === leftFixedKey) : -1;
const hasData = dataSource.length > 0;
let months = effectiveMonths;
// 静的列の描画定義を作る共通関数
const buildStaticCol = (col: (typeof visibleCols)[number], fixed: 'left' | 'right' | undefined) => {
const dataIndex = col.dataIndex as keyof ProposalData;
const headerWidth = calcTextWidth(col.title);
const dataWidth = dataSource.reduce((max, row) => Math.max(max, calcTextWidth(String(row[dataIndex] ?? ''))), 0);
const colWidth = Math.max(headerWidth, dataWidth) + 56;
return {
title: col.title,
dataIndex: col.dataIndex,
key: col.key,
width: colWidth,
...(fixed ? { fixed } : {}),
onCell: () => ({ style: { whiteSpace: 'nowrap' } }),
onHeaderCell: () => ({ style: { whiteSpace: 'nowrap' } }),
sorter: hasData
? (a: ProposalData, b: ProposalData) =>
localeCompareJa(String(a[dataIndex] ?? ''), String(b[dataIndex] ?? ''))
: (false as const),
...(hasData
? {
filterDropdown: (props: FilterDropdownProps) => (
<VirtualFilterDropdown {...props} filters={generateColumnFilters(dataSource, dataIndex)} />
),
onFilter: (value: React.Key | boolean, record: ProposalData) =>
matchColumnFilter(record[dataIndex], [value as React.Key]),
}
: {}),
};
};
// ============ 期間軸表示:上=月、下=各項目(月を主軸に列を展開) ============
if (layout === 'period') {
const staticVisibleCols = visibleCols.filter((col) => !col.monthly);
const monthlyVisibleCols = visibleCols.filter((col) => col.monthly);
// leftFixedKey が静的列の場合、その静的列までを左固定にする。
// const staticLeftIndex =
// leftFixedKey && !PROPOSAL_COLUMNS.find((c) => c.key === leftFixedKey)?.monthly
// ? staticVisibleCols.findIndex((c) => c.key === leftFixedKey)
// : -1;
// 静的列を「左固定・通常」に振り分け
const leftStaticCols: TableColumnsType<ProposalData> = [];
const normalStaticCols: TableColumnsType<ProposalData> = [];
staticVisibleCols.forEach((col) => {
if (leftFixedKey?.includes(col.key)) {
leftStaticCols.push(buildStaticCol(col, 'left'));
} else {
normalStaticCols.push(buildStaticCol(col, undefined));
}
});
// 月ごとにグループ化:親=月、子=各月次項目
const monthGroups = months.map((m) => {
const isCurrent = m === CURRENT_MONTH;
const childrenCols = monthlyVisibleCols
.filter((col) => !currentMonthKey?.includes(col.key))
.map((col) => {
const monthKey = `${col.key}__${m}`;
const isNumeric = col.numeric;
const headerWidth = calcTextWidth(col.title);
const dataWidth = dataSource.reduce(
(max, row) => Math.max(max, calcTextWidth(String(row.monthly?.[monthKey] ?? ''))),
0
);
const colWidth = Math.max(headerWidth, dataWidth) + 40;
const isRightFixed = !!rightFixedKey?.includes(col.key);
return {
title: col.title,
key: monthKey,
dataIndex: ['monthly', monthKey],
width: colWidth,
fixed: isRightFixed ? ('right' as const) : undefined,
align: isNumeric ? ('right' as const) : undefined,
onCell: () => ({
style: {
whiteSpace: 'nowrap',
...(isNumeric ? { textAlign: 'right' as const } : {}),
...(isCurrent ? { backgroundColor: '#fafcff' } : {}),
},
}),
onHeaderCell: () => ({
style: { whiteSpace: 'nowrap', ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) },
}),
render: (_: unknown, record: ProposalData) => record.monthly?.[monthKey] ?? '',
sorter: hasData
? (a: ProposalData, b: ProposalData) =>
localeCompareJa(String(a.monthly?.[monthKey] ?? ''), String(b.monthly?.[monthKey] ?? ''))
: false,
};
});
return {
title: isCurrent ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600 }}>{monthLabel(m)}</span>
<span
style={{
fontSize: 10,
lineHeight: '16px',
padding: '0 6px',
color: '#1677ff',
background: '#f0f7ff',
borderRadius: 8,
fontWeight: 400,
}}
>
当月
</span>
</span>
) : (
monthLabel(m)
),
key: `group_${m}`,
onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
children: childrenCols,
};
});
// 当月
const currentMonth = ['202610'];
// 当月的一定不在右侧固定列里面
const currentMonthGroups =
currentMonthKey && currentMonthKey.length > 0
? currentMonth.map((m) => {
const isCurrent = m === CURRENT_MONTH;
const childrenCols = monthlyVisibleCols
.filter((col) => currentMonthKey?.includes(col.key))
.map((col) => {
const monthKey = `${col.key}__${m}`;
const isNumeric = col.numeric;
const headerWidth = calcTextWidth(col.title);
const dataWidth = dataSource.reduce(
(max, row) => Math.max(max, calcTextWidth(String(row.monthly?.[monthKey] ?? ''))),
0
);
const colWidth = Math.max(headerWidth, dataWidth) + 40;
const isRightFixed = !!rightFixedKey?.includes(col.key);
return {
title: col.title,
key: monthKey,
dataIndex: ['monthly', monthKey],
width: colWidth,
fixed: isRightFixed ? ('right' as const) : undefined,
align: isNumeric ? ('right' as const) : undefined,
onCell: () => ({
style: {
whiteSpace: 'nowrap',
...(isNumeric ? { textAlign: 'right' as const } : {}),
...(isCurrent ? { backgroundColor: '#fafcff' } : {}),
},
}),
onHeaderCell: () => ({
style: { whiteSpace: 'nowrap', ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) },
}),
render: (_: unknown, record: ProposalData) => record.monthly?.[monthKey] ?? '',
sorter: hasData
? (a: ProposalData, b: ProposalData) =>
localeCompareJa(String(a.monthly?.[monthKey] ?? ''), String(b.monthly?.[monthKey] ?? ''))
: false,
};
});
return {
title: isCurrent ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600 }}>{monthLabel(m)}</span>
<span
style={{
fontSize: 10,
lineHeight: '16px',
padding: '0 6px',
color: '#1677ff',
background: '#f0f7ff',
borderRadius: 8,
fontWeight: 400,
}}
>
当月
</span>
</span>
) : (
monthLabel(m)
),
key: `group_${m}`,
onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
children: childrenCols,
};
})
: [];
const operationColumn = {
title: '操作',
key: 'operation',
width: 57,
fixed: 'right' as const,
align: 'center' as const,
onCell: () => ({ style: { textAlign: 'center' as const } }),
onHeaderCell: () => ({ style: { textAlign: 'center' as const } }),
render: renderOperation,
};
console.log('monthGroups', monthGroups);
// 过滤出右侧的列
const rightItemsCols =
rightFixedKey && rightFixedKey.length > 0
? monthGroups.map((item) => {
return {
...item,
fixed: 'right',
children: item.children.filter((child) => child.fixed === 'right'),
};
})
: [];
const monthlyItemsColsWithoutRight = monthGroups.map((item) => {
return {
...item,
children: item.children.filter((child) => !child.fixed),
};
});
console.log('rightItemsCols', rightItemsCols);
// 左固定 → 通常静的 → 当月→ 月グループ → 操作列(右固定)の順で連続させる
return [
...leftStaticCols,
...normalStaticCols,
...currentMonthGroups,
...monthlyItemsColsWithoutRight,
...rightItemsCols,
operationColumn,
] as TableColumnsType<ProposalData>;
}
// ============ 項目軸表示:行=要員、月次列は月ごとにサブ列展開 ============
// 【列順を期間軸と統一】静的項目を先、月次項目を後に並べる。
// 固定列は静的項目のみ。右固定の静的列は右端(操作列の前)へ寄せて連続させる。
const leftItemCols: TableColumnsType<ProposalData> = [];
const normalStaticItemCols: TableColumnsType<ProposalData> = [];
const monthlyItemCols: TableColumnsType<ProposalData> = [];
visibleCols.forEach((col) => {
if (currentMonthKey?.includes(col.key)) {
months = [CURRENT_MONTH];
} else {
months = effectiveMonths;
}
// --- 月次項目:選択された月ごとにサブ列を展開(固定しない・通常列扱い) ---
if (col.monthly) {
const headerWidth = calcTextWidth(col.title);
monthlyItemCols.push({
title: col.title,
key: col.key,
fixed: rightFixedKey?.includes(col.key) ? 'right' : undefined,
children: months.map((m) => {
const monthKey = `${col.key}__${m}`;
const isCurrent = m === CURRENT_MONTH;
const isNumeric = col.numeric;
const dataWidth = dataSource.reduce(
(max, row) => Math.max(max, calcTextWidth(String(row.monthly?.[monthKey] ?? ''))),
0
);
const colWidth = Math.max(headerWidth, calcTextWidth(monthLabel(m)) + (isCurrent ? 44 : 0), dataWidth) + 40;
return {
title: isCurrent ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600 }}>{monthLabel(m)}</span>
<span
style={{
fontSize: 10,
lineHeight: '16px',
padding: '0 6px',
color: '#1677ff',
background: '#f0f7ff',
borderRadius: 8,
fontWeight: 400,
}}
>
当月
</span>
</span>
) : (
monthLabel(m)
),
key: monthKey,
dataIndex: ['monthly', monthKey],
width: colWidth,
fixed: rightFixedKey?.includes(col.key) ? 'right' : undefined,
align: isNumeric ? ('right' as const) : undefined,
onCell: () => ({
style: {
whiteSpace: 'nowrap',
...(isNumeric ? { textAlign: 'right' as const } : {}),
...(isCurrent ? { backgroundColor: '#fafcff' } : {}),
},
}),
onHeaderCell: () => ({
style: { whiteSpace: 'nowrap', ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) },
}),
render: (_: unknown, record: ProposalData) => record.monthly?.[monthKey] ?? '',
// 全ページ共通:localeCompareJa(数字感知)で統一
sorter: hasData
? (a: ProposalData, b: ProposalData) =>
localeCompareJa(String(a.monthly?.[monthKey] ?? ''), String(b.monthly?.[monthKey] ?? ''))
: false,
};
}),
});
return;
}
// --- 静的項目:左固定 / 通常 に振り分け ---
// if (leftIndex >= 0 && idx <= leftIndex) {
// leftItemCols.push(buildStaticCol(col, 'left'));
// } else {
// normalStaticItemCols.push(buildStaticCol(col, undefined));
// }
if (leftFixedKey?.includes(col.key)) {
leftItemCols.push(buildStaticCol(col, 'left'));
} else {
normalStaticItemCols.push(buildStaticCol(col, undefined));
}
});
// 操作列(右固定):共通の useOperationColumn を使用
const operationColumn = {
title: '操作',
key: 'operation',
width: 57,
fixed: 'right' as const,
align: 'center' as const,
onCell: () => ({ style: { textAlign: 'center' as const } }),
onHeaderCell: () => ({ style: { textAlign: 'center' as const } }),
render: renderOperation,
};
console.log('鱼分monthlyItemCols', monthlyItemCols);
// 过滤出当月的列
const currentMonthCols = monthlyItemCols.filter((item) => currentMonthKey?.includes(item.key as string));
const totalMonthCols = monthlyItemCols.filter(
(item) => !currentMonthKey?.includes(item.key as string) && !item.fixed
);
// 过滤出右侧的列
const rightItemsCols = monthlyItemCols.filter((item) => item.fixed === 'right') ?? [];
console.log('当月列', currentMonthCols);
console.log('当月列', totalMonthCols);
// 期間軸と列順を統一:左固定 → 通常静的 → 当月→ 月次項目 → 右固定 →操作列(右固定)
return [
...leftItemCols,
...normalStaticItemCols,
...currentMonthCols,
...totalMonthCols,
...rightItemsCols,
operationColumn,
];
}, [
visibleColumnKeys,
leftFixedKey,
rightFixedKey,
currentMonthKey,
dataSource,
effectiveMonths,
layout,
renderOperation,
]);

浙公网安备 33010602011771号