feat: SPEC-36H addendum — status filter, overdue sort, overdue column in upcoming payments
Radio.Group filter: Все/Ожидает/Одобрен/Планируется. Sort mode: По дате / Просроченные ↑ (overdue first). New column "Просрочка": shows N дн. in red for past-due payments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
80978df991
commit
5400b9328c
@ -17,8 +17,13 @@ Succes. 2 fichiers modifies (CashflowTab.tsx, PaymentsTab.tsx).
|
||||
- Arret apres 10s : OUI (setTimeout → setBlinkingId(null))
|
||||
- Tab key Счета : `payments` (dans DocumentsPage.tsx)
|
||||
|
||||
## 4. COMPILATION
|
||||
## 4. FILTRES ET TRI
|
||||
- Filtre par statut (Radio.Group) : OUI — Все / Ожидает / Одобрен / Планируется
|
||||
- Tri "Просроченные ↑" : OUI — overdue en tete, puis par date
|
||||
- Colonne "Просрочка" (N дн. en rouge) : OUI — calcul diffDays, affiche si > 0
|
||||
|
||||
## 5. COMPILATION
|
||||
- Frontend tsc : 0 erreurs
|
||||
|
||||
## 5. COMMITS
|
||||
## 6. COMMITS
|
||||
A commiter
|
||||
|
||||
@ -188,6 +188,8 @@ export function CashflowTab() {
|
||||
const [manualSubmitting, setManualSubmitting] = useState(false);
|
||||
const [manualForm] = Form.useForm();
|
||||
const [upcomingDateRange, setUpcomingDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
|
||||
const [upcomingStatusFilter, setUpcomingStatusFilter] = useState<string>('ALL');
|
||||
const [upcomingSortMode, setUpcomingSortMode] = useState<'date' | 'overdue'>('date');
|
||||
|
||||
// ---- Fetch functions ----
|
||||
|
||||
@ -806,15 +808,35 @@ export function CashflowTab() {
|
||||
|
||||
// ---- Filtered upcoming payments ----
|
||||
const filteredUpcoming = useMemo(() => {
|
||||
const items = dashboard?.upcomingPayments ?? [];
|
||||
if (!upcomingDateRange || !upcomingDateRange[0] || !upcomingDateRange[1]) return items;
|
||||
const from = upcomingDateRange[0].startOf('day');
|
||||
const to = upcomingDateRange[1].endOf('day');
|
||||
return items.filter(p => {
|
||||
const d = dayjs(p.date);
|
||||
return d.isAfter(from.subtract(1, 'ms')) && d.isBefore(to.add(1, 'ms'));
|
||||
});
|
||||
}, [dashboard?.upcomingPayments, upcomingDateRange]);
|
||||
let items = [...(dashboard?.upcomingPayments ?? [])];
|
||||
// Date range filter
|
||||
if (upcomingDateRange && upcomingDateRange[0] && upcomingDateRange[1]) {
|
||||
const from = upcomingDateRange[0].startOf('day');
|
||||
const to = upcomingDateRange[1].endOf('day');
|
||||
items = items.filter(p => {
|
||||
const d = dayjs(p.date);
|
||||
return d.isAfter(from.subtract(1, 'ms')) && d.isBefore(to.add(1, 'ms'));
|
||||
});
|
||||
}
|
||||
// Status filter
|
||||
if (upcomingStatusFilter !== 'ALL') {
|
||||
items = items.filter(p => p.status === upcomingStatusFilter);
|
||||
}
|
||||
// Sort
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
if (upcomingSortMode === 'overdue') {
|
||||
items.sort((a, b) => {
|
||||
const aOver = new Date(a.date) < now ? 1 : 0;
|
||||
const bOver = new Date(b.date) < now ? 1 : 0;
|
||||
if (aOver !== bOver) return bOver - aOver;
|
||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
});
|
||||
} else {
|
||||
items.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
}
|
||||
return items;
|
||||
}, [dashboard?.upcomingPayments, upcomingDateRange, upcomingStatusFilter, upcomingSortMode]);
|
||||
|
||||
const upcomingTotal = useMemo(() => filteredUpcoming.reduce((s, p) => s + p.amount, 0), [filteredUpcoming]);
|
||||
|
||||
@ -834,6 +856,13 @@ export function CashflowTab() {
|
||||
{ title: 'Сумма', dataIndex: 'amount', key: 'amount', width: 130, align: 'right', render: (v: number) => formatAmount(v) },
|
||||
{ title: 'Статус', dataIndex: 'status', key: 'status', width: 120,
|
||||
render: (v: string) => <Tag color={upcomingStatusColors[v] ?? 'default'}>{upcomingStatusLabels[v] ?? v}</Tag> },
|
||||
{ title: 'Просрочка', key: 'overdue', width: 100,
|
||||
render: (_: unknown, r) => {
|
||||
const now = new Date(); now.setHours(0, 0, 0, 0);
|
||||
const due = new Date(r.date); due.setHours(0, 0, 0, 0);
|
||||
const diff = Math.floor((now.getTime() - due.getTime()) / (1000 * 60 * 60 * 24));
|
||||
return diff > 0 ? <span style={{ color: '#ff4d4f', fontWeight: 600 }}>{diff} дн.</span> : null;
|
||||
} },
|
||||
{ title: 'ЗП', dataIndex: 'poNumber', key: 'po', width: 130,
|
||||
render: (v: string, r) => r.poId ? <a onClick={(e) => { e.stopPropagation(); window.location.href = `/purchases?tab=orders&po=${r.poId}`; }}>{v}</a> : v },
|
||||
];
|
||||
@ -1167,20 +1196,36 @@ export function CashflowTab() {
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
children: filteredUpcoming.length > 0 ? (
|
||||
<Table
|
||||
columns={upcomingColumns}
|
||||
dataSource={filteredUpcoming}
|
||||
rowKey={(r) => `${r.type}-${r.id}`}
|
||||
pagination={{ pageSize: 10, size: 'small', showSizeChanger: true, pageSizeOptions: ['10', '25', '50'] }}
|
||||
size="small"
|
||||
onRow={(r) => ({
|
||||
style: { cursor: r.poId ? 'pointer' : undefined },
|
||||
onClick: () => { if (r.poId) window.location.href = `/purchases?tab=orders&po=${r.poId}`; },
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: 16, color: '#999', textAlign: 'center' }}>Нет ближайших платежей</div>
|
||||
children: (
|
||||
<>
|
||||
<Space style={{ marginBottom: 12 }} wrap>
|
||||
<Radio.Group value={upcomingStatusFilter} onChange={(e) => setUpcomingStatusFilter(e.target.value)} size="small">
|
||||
<Radio.Button value="ALL">Все</Radio.Button>
|
||||
<Radio.Button value="PENDING">Ожидает</Radio.Button>
|
||||
<Radio.Button value="APPROVED">Одобрен</Radio.Button>
|
||||
<Radio.Button value="PLANNED">Планируется</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Radio.Group value={upcomingSortMode} onChange={(e) => setUpcomingSortMode(e.target.value)} size="small">
|
||||
<Radio.Button value="date">По дате</Radio.Button>
|
||||
<Radio.Button value="overdue">Просроченные ↑</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
{filteredUpcoming.length > 0 ? (
|
||||
<Table
|
||||
columns={upcomingColumns}
|
||||
dataSource={filteredUpcoming}
|
||||
rowKey={(r) => `${r.type}-${r.id}`}
|
||||
pagination={{ pageSize: 10, size: 'small', showSizeChanger: true, pageSizeOptions: ['10', '25', '50'] }}
|
||||
size="small"
|
||||
onRow={(r) => ({
|
||||
style: { cursor: r.poId ? 'pointer' : undefined },
|
||||
onClick: () => { if (r.poId) window.location.href = `/purchases?tab=orders&po=${r.poId}`; },
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: 16, color: '#999', textAlign: 'center' }}>Нет ближайших платежей</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}]}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user