import { useState, useMemo, useRef, useCallback } from 'react'; import { useStore } from '../store'; import { getAvailableMonths, filterByMonth } from '../utils/bookkeeping'; // 列定義 const COLUMNS = [ { key: 'date', label: '日付', defaultWidth: 96, align: 'left' }, { key: 'desc', label: '摘要', defaultWidth: 200, align: 'left' }, { key: 'debitAcc', label: '借方勘定', defaultWidth: 130, align: 'left' }, { key: 'debitAmt', label: '借方金額', defaultWidth: 90, align: 'right' }, { key: 'creditAcc', label: '貸方勘定', defaultWidth: 130, align: 'left' }, { key: 'creditAmt', label: '貸方金額', defaultWidth: 90, align: 'right' }, { key: 'mfcat', label: 'MF分類', defaultWidth: 140, align: 'left' }, ] as const; type ColKey = typeof COLUMNS[number]['key']; function useColumnResize(defaults: Record) { const [widths, setWidths] = useState>(defaults); const dragging = useRef<{ key: ColKey; startX: number; startW: number } | null>(null); const onMouseDown = useCallback((key: ColKey, e: React.MouseEvent) => { e.preventDefault(); dragging.current = { key, startX: e.clientX, startW: widths[key] }; const onMove = (ev: MouseEvent) => { if (!dragging.current) return; const delta = ev.clientX - dragging.current.startX; const newW = Math.max(50, dragging.current.startW + delta); setWidths(prev => ({ ...prev, [dragging.current!.key]: newW })); }; const onUp = () => { dragging.current = null; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); }, [widths]); return { widths, onMouseDown }; } const DEFAULT_WIDTHS = Object.fromEntries( COLUMNS.map(c => [c.key, c.defaultWidth]) ) as Record; export default function JournalPage() { const { transactions, accounts, selectedMonth, setSelectedMonth } = useStore(); const accountMap = useMemo(() => new Map(accounts.map(a => [a.id, a])), [accounts]); const availableMonths = useMemo(() => getAvailableMonths(transactions), [transactions]); const effectiveMonth = selectedMonth || availableMonths[0] || ''; const [search, setSearch] = useState(''); const { widths, onMouseDown } = useColumnResize(DEFAULT_WIDTHS); const [year, month] = effectiveMonth ? [parseInt(effectiveMonth.split('-')[0]), parseInt(effectiveMonth.split('-')[1])] : [0, 0]; const filtered = useMemo(() => { let txs = effectiveMonth ? filterByMonth(transactions, year, month) : transactions; if (search) { const q = search.toLowerCase(); txs = txs.filter( t => t.description.toLowerCase().includes(q) || t.mfCategory?.includes(q) || t.mfSubCategory?.includes(q) || t.entries.some(e => accountMap.get(e.accountId)?.name.toLowerCase().includes(q)) ); } return [...txs].reverse(); }, [transactions, effectiveMonth, year, month, search]); if (transactions.length === 0) { return (

📒

データがありません。CSVをインポートしてください。

); } return (

仕訳帳

setSearch(e.target.value)} className="border rounded-md px-3 py-1.5 text-sm w-48" />

{filtered.length}件

a + b, 0) }}> {COLUMNS.map(col => ( ))} {COLUMNS.map((col, i) => ( ))} {filtered.map(tx => { const debits = tx.entries.filter(e => e.debit > 0); const credits = tx.entries.filter(e => e.credit > 0); const maxRows = Math.max(debits.length, credits.length); return Array.from({ length: maxRows }, (_, i) => ( {i === 0 && ( <> )} {i === 0 && ( )} )); })}
{col.label} {/* リサイズハンドル(最後の列以外) */} {i < COLUMNS.length - 1 && (
onMouseDown(col.key, e)} className="absolute top-0 right-0 h-full w-3 flex items-center justify-center cursor-col-resize group" >
)}
{tx.date}
{tx.description}
{tx.mfMemo &&
{tx.mfMemo}
}
{debits[i] ? accountMap.get(debits[i].accountId)?.name : ''} {debits[i] ? debits[i].debit.toLocaleString() : ''} {credits[i] ? accountMap.get(credits[i].accountId)?.name : ''} {credits[i] ? credits[i].credit.toLocaleString() : ''} {[tx.mfCategory, tx.mfSubCategory].filter(Boolean).join(' / ')}
); }