Initial commit MKD fixes
This commit is contained in:
515
components/finance/PaymentCalendar.tsx
Executable file
515
components/finance/PaymentCalendar.tsx
Executable file
@@ -0,0 +1,515 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Invoice,
|
||||
InvoiceStatus,
|
||||
PaymentInvoice,
|
||||
PaymentCalendarEntry,
|
||||
PaymentDirection,
|
||||
FinanceAccount
|
||||
} from '../../types';
|
||||
import { apiClient } from '../../services/apiClient';
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
Wallet,
|
||||
History,
|
||||
Pause,
|
||||
XCircle,
|
||||
Plus,
|
||||
ArrowDownCircle,
|
||||
ArrowUpCircle,
|
||||
Banknote,
|
||||
Landmark
|
||||
} from 'lucide-react';
|
||||
import { PaymentCalendarEntryForm } from './PaymentCalendarEntryForm';
|
||||
import { PaymentStatusModal, PaymentStatusAction, PaymentStatusModalPayload } from './PaymentStatusModal';
|
||||
|
||||
type CalendarInterval = 'week' | 'month';
|
||||
|
||||
function getPeriodBounds(interval: CalendarInterval): { dateFrom: string; dateTo: string; label: string } {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
if (interval === 'week') {
|
||||
const day = now.getDay();
|
||||
const mondayOffset = day === 0 ? -6 : 1 - day;
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + mondayOffset);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
const dateFrom = monday.toISOString().split('T')[0];
|
||||
const dateTo = sunday.toISOString().split('T')[0];
|
||||
const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short' };
|
||||
const label = `${monday.toLocaleDateString('ru-RU', options)} – ${sunday.toLocaleDateString('ru-RU', options)} ${year}`;
|
||||
return { dateFrom, dateTo, label };
|
||||
}
|
||||
const first = new Date(year, month, 1);
|
||||
const last = new Date(year, month + 1, 0);
|
||||
const dateFrom = first.toISOString().split('T')[0];
|
||||
const dateTo = last.toISOString().split('T')[0];
|
||||
const label = first.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' });
|
||||
return { dateFrom, dateTo, label };
|
||||
}
|
||||
|
||||
interface PaymentCalendarProps {
|
||||
invoices?: Invoice[];
|
||||
paymentInvoices?: PaymentInvoice[];
|
||||
calendarEntries?: PaymentCalendarEntry[];
|
||||
onUpdateStatus?: (id: string, s: InvoiceStatus, extra?: any) => void;
|
||||
onUpdatePaymentInvoiceStatus?: (id: number, status: 'paid' | 'postponed' | 'cancelled', payload?: PaymentStatusModalPayload) => Promise<void>;
|
||||
onRefreshCalendar?: () => void;
|
||||
currentBalance: number;
|
||||
currentUserId: string;
|
||||
interval?: CalendarInterval;
|
||||
onIntervalChange?: (interval: CalendarInterval) => void;
|
||||
periodLabel?: string;
|
||||
}
|
||||
|
||||
type RowItem = {
|
||||
id: string;
|
||||
direction: PaymentDirection;
|
||||
amount: number;
|
||||
scheduledDate: string;
|
||||
contractorName: string;
|
||||
description: string;
|
||||
probability?: string;
|
||||
isCash?: boolean;
|
||||
status?: string;
|
||||
paymentInvoiceId?: number;
|
||||
isOld?: boolean;
|
||||
type: 'invoice' | 'entry';
|
||||
entryId?: number;
|
||||
currency?: string;
|
||||
};
|
||||
|
||||
export const PaymentCalendar: React.FC<PaymentCalendarProps> = ({
|
||||
invoices = [],
|
||||
paymentInvoices = [],
|
||||
calendarEntries = [],
|
||||
onUpdateStatus,
|
||||
onUpdatePaymentInvoiceStatus,
|
||||
onRefreshCalendar,
|
||||
currentBalance,
|
||||
currentUserId,
|
||||
interval = 'month',
|
||||
onIntervalChange,
|
||||
periodLabel: periodLabelProp
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<'all' | 'planned' | 'paid'>('all');
|
||||
const [directionFilter, setDirectionFilter] = useState<'all' | 'outgoing' | 'incoming'>('all');
|
||||
const [loading, setLoading] = useState<Record<number, boolean>>({});
|
||||
const [showEntryForm, setShowEntryForm] = useState(false);
|
||||
const [defaultFormDirection, setDefaultFormDirection] = useState<PaymentDirection>('outgoing');
|
||||
const [statusModal, setStatusModal] = useState<{ action: PaymentStatusAction; row: RowItem } | null>(null);
|
||||
const [financeAccounts, setFinanceAccounts] = useState<FinanceAccount[]>([]);
|
||||
|
||||
const { dateFrom, dateTo, label } = useMemo(() => getPeriodBounds(interval), [interval]);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const list = await apiClient.get<FinanceAccount[]>('/finance/accounts');
|
||||
setFinanceAccounts(Array.isArray(list) ? list : []);
|
||||
} catch {
|
||||
setFinanceAccounts([]);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
const periodLabel = periodLabelProp ?? label;
|
||||
|
||||
const invoiceRows: RowItem[] = useMemo(() => {
|
||||
const invList = [
|
||||
...invoices.map((inv) => ({
|
||||
id: inv.id,
|
||||
direction: 'outgoing' as PaymentDirection,
|
||||
amount: Number(inv.amount) || 0,
|
||||
scheduledDate: inv.scheduledDate || '',
|
||||
contractorName: inv.contractorName,
|
||||
description: inv.serviceName,
|
||||
status: inv.status,
|
||||
isOld: true,
|
||||
type: 'invoice' as const,
|
||||
paymentInvoiceId: undefined
|
||||
})),
|
||||
...paymentInvoices.map((inv) => ({
|
||||
id: `pi-${inv.id}`,
|
||||
direction: 'outgoing' as PaymentDirection,
|
||||
amount: Number(inv.totalAmount) || 0,
|
||||
scheduledDate: inv.scheduledDate || '',
|
||||
contractorName: inv.contractorName,
|
||||
description: inv.serviceDescription,
|
||||
status: inv.status,
|
||||
isOld: false,
|
||||
type: 'invoice' as const,
|
||||
paymentInvoiceId: inv.id
|
||||
}))
|
||||
];
|
||||
return invList;
|
||||
}, [invoices, paymentInvoices]);
|
||||
|
||||
const entryRows: RowItem[] = useMemo(() => {
|
||||
const invoiceIds = new Set(paymentInvoices.map((pi) => pi.id));
|
||||
return calendarEntries
|
||||
.filter((e) => !e.paymentInvoiceId || !invoiceIds.has(e.paymentInvoiceId))
|
||||
.map((e) => ({
|
||||
id: `entry-${e.id}`,
|
||||
direction: e.direction,
|
||||
amount: Number(e.amount) || 0,
|
||||
scheduledDate: e.scheduledDate,
|
||||
contractorName: e.contractorName,
|
||||
description: e.description || e.category,
|
||||
probability: e.probability,
|
||||
isCash: e.isCash,
|
||||
type: 'entry' as const,
|
||||
entryId: e.id,
|
||||
currency: e.currency
|
||||
}));
|
||||
}, [calendarEntries, paymentInvoices]);
|
||||
|
||||
const allRows = useMemo(() => {
|
||||
const combined = [...invoiceRows, ...entryRows];
|
||||
return combined
|
||||
.filter((r) => {
|
||||
if (directionFilter !== 'all' && r.direction !== directionFilter) return false;
|
||||
if (filter === 'planned') {
|
||||
if (r.type === 'invoice') return r.status === 'scheduled' || r.status === 'overdue';
|
||||
return !r.scheduledDate ? false : true;
|
||||
}
|
||||
if (filter === 'paid') {
|
||||
if (r.type === 'invoice') return r.status === 'paid';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => (a.scheduledDate || '').localeCompare(b.scheduledDate || ''));
|
||||
}, [invoiceRows, entryRows, filter, directionFilter]);
|
||||
|
||||
const scheduledTotalOutgoing =
|
||||
invoiceRows.filter((i) => i.status === 'scheduled').reduce((s, i) => s + Number(i.amount) || 0, 0) +
|
||||
entryRows.filter((e) => e.direction === 'outgoing').reduce((s, e) => s + Number(e.amount) || 0, 0);
|
||||
const scheduledTotalIncoming = entryRows
|
||||
.filter((e) => e.direction === 'incoming')
|
||||
.reduce((s, e) => s + Number(e.amount) || 0, 0);
|
||||
|
||||
const handlePaymentActionClick = (row: RowItem, action: 'paid' | 'postponed' | 'cancelled') => {
|
||||
if (row.type !== 'invoice' || row.isOld) {
|
||||
if (onUpdateStatus) onUpdateStatus(row.id, action === 'paid' ? 'paid' : 'scheduled');
|
||||
return;
|
||||
}
|
||||
if (row.paymentInvoiceId && onUpdatePaymentInvoiceStatus) {
|
||||
setStatusModal({ action, row });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusModalConfirm = async (payload: PaymentStatusModalPayload) => {
|
||||
if (!statusModal || !statusModal.row.paymentInvoiceId || !onUpdatePaymentInvoiceStatus) return;
|
||||
const { row, action } = statusModal;
|
||||
const id = row.paymentInvoiceId!;
|
||||
setStatusModal(null);
|
||||
setLoading((prev) => ({ ...prev, [id]: true }));
|
||||
try {
|
||||
await onUpdatePaymentInvoiceStatus(id, action, payload);
|
||||
} catch (err) {
|
||||
console.error('Error updating payment invoice status:', err);
|
||||
alert('Ошибка при обновлении статуса');
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, [id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEntry = async (entryId: number) => {
|
||||
if (!window.confirm('Удалить запись из календаря?')) return;
|
||||
try {
|
||||
await apiClient.delete(`/finance/payment-calendar/entries/${entryId}`);
|
||||
onRefreshCalendar?.();
|
||||
} catch (err) {
|
||||
console.error('Error deleting entry:', err);
|
||||
alert('Ошибка удаления');
|
||||
}
|
||||
};
|
||||
|
||||
const probabilityLabel: Record<string, string> = {
|
||||
confirmed: '100%',
|
||||
high: '80%',
|
||||
medium: '50%',
|
||||
low: '30%'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl w-fit">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all ${filter === 'all' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('planned')}
|
||||
className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all ${filter === 'planned' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
План
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('paid')}
|
||||
className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all ${filter === 'paid' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
Оплачено
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl w-fit">
|
||||
<button
|
||||
onClick={() => setDirectionFilter('all')}
|
||||
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-all ${directionFilter === 'all' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDirectionFilter('outgoing')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 text-xs font-bold rounded-lg transition-all ${directionFilter === 'outgoing' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
<ArrowUpCircle className="w-3.5 h-3.5" /> Расходы
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDirectionFilter('incoming')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 text-xs font-bold rounded-lg transition-all ${directionFilter === 'incoming' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
<ArrowDownCircle className="w-3.5 h-3.5" /> Поступления
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{onIntervalChange && (
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||
<button
|
||||
onClick={() => onIntervalChange('week')}
|
||||
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-all ${interval === 'week' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
Неделя
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onIntervalChange('month')}
|
||||
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-all ${interval === 'month' ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
||||
>
|
||||
Месяц
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{financeAccounts.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{financeAccounts.filter((a) => a.type === 'bank').map((a) => (
|
||||
<div key={a.id} className="flex items-center gap-2 px-3 py-2 bg-slate-100 rounded-xl border border-slate-200">
|
||||
<Landmark className="w-4 h-4 text-slate-500" />
|
||||
<div>
|
||||
<p className="text-[10px] text-slate-500 font-bold uppercase">{a.name}</p>
|
||||
<p className="text-sm font-black text-slate-800">{Number(a.balance).toLocaleString('ru-RU')} ₽</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{financeAccounts.filter((a) => a.type === 'cash').map((a) => (
|
||||
<div key={a.id} className="flex items-center gap-2 px-3 py-2 bg-amber-50 rounded-xl border border-amber-200">
|
||||
<Banknote className="w-4 h-4 text-amber-600" />
|
||||
<div>
|
||||
<p className="text-[10px] text-amber-700 font-bold uppercase">{a.name}</p>
|
||||
<p className="text-sm font-black text-amber-800">{Number(a.balance).toLocaleString('ru-RU')} ₽</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-black uppercase">Итого: {financeAccounts.reduce((s, a) => s + Number(a.balance), 0).toLocaleString('ru-RU')} ₽</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">Текущий остаток</p>
|
||||
<p className="text-sm font-black text-slate-800">{currentBalance.toLocaleString()} ₽</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setDefaultFormDirection('outgoing');
|
||||
setShowEntryForm(true);
|
||||
}}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-slate-900 text-white rounded-xl text-xs font-bold hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-4 bg-slate-900 text-white flex flex-wrap justify-between items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-white/10 rounded-lg">
|
||||
<Calendar className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold leading-none uppercase tracking-tighter">Платежный календарь</h3>
|
||||
<p className="text-[10px] text-slate-400 mt-1 uppercase font-bold tracking-widest">{periodLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-black">{Number(scheduledTotalOutgoing).toLocaleString('ru-RU')} ₽</p>
|
||||
<p className="text-[10px] text-slate-400 uppercase font-bold">К оплате (план)</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-black text-emerald-300">+{Number(scheduledTotalIncoming).toLocaleString('ru-RU')} ₽</p>
|
||||
<p className="text-[10px] text-slate-400 uppercase font-bold">Ожидаемые поступления</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{allRows.length === 0 && (
|
||||
<div className="p-12 text-center text-slate-400 italic">Нет записей за выбранный период</div>
|
||||
)}
|
||||
{allRows.map((row) => {
|
||||
const isPaid = row.type === 'invoice' && row.status === 'paid';
|
||||
const isOverdue = row.type === 'invoice' && row.status === 'overdue';
|
||||
const isIncoming = row.direction === 'incoming';
|
||||
const day = row.scheduledDate?.split('-')[2] || '??';
|
||||
const monthShort = row.scheduledDate
|
||||
? new Date(row.scheduledDate + 'T12:00:00').toLocaleDateString('ru-RU', { month: 'short' })
|
||||
: '—';
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className={`p-4 flex items-center gap-4 hover:bg-slate-50 transition-colors group ${isPaid ? 'bg-slate-50/50' : ''}`}
|
||||
>
|
||||
<div
|
||||
className={`p-2 rounded-xl text-center min-w-[50px] border ${
|
||||
isPaid
|
||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600'
|
||||
: isOverdue
|
||||
? 'bg-red-50 border-red-100 text-red-600'
|
||||
: isIncoming
|
||||
? 'bg-emerald-50/50 border-emerald-100 text-emerald-700'
|
||||
: 'bg-white border-slate-200 text-slate-500 shadow-sm'
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs font-black leading-none">{day}</p>
|
||||
<p className="text-[9px] font-bold uppercase mt-1">{monthShort}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
{isIncoming && <ArrowDownCircle className="w-3.5 h-3.5 text-emerald-500 shrink-0" />}
|
||||
{row.isCash && <Banknote className="w-3.5 h-3.5 text-amber-600 shrink-0" title="Наличные" />}
|
||||
<p
|
||||
className={`text-sm font-bold truncate ${isPaid ? 'text-slate-500' : 'text-slate-800'}`}
|
||||
>
|
||||
{row.contractorName || '—'}
|
||||
</p>
|
||||
{row.type === 'invoice' && isPaid && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" />}
|
||||
{row.probability && row.probability !== 'confirmed' && (
|
||||
<span className="text-[9px] font-bold text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
||||
{probabilityLabel[row.probability] || row.probability}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 truncate">{row.description || '—'}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right flex items-center gap-4">
|
||||
<div>
|
||||
<p
|
||||
className={`text-sm font-black ${isPaid ? 'text-slate-400' : isIncoming ? 'text-emerald-700' : 'text-slate-900'}`}
|
||||
>
|
||||
{isIncoming ? '+' : ''}
|
||||
{row.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '}
|
||||
{row.currency === 'RUB' || !row.currency ? '₽' : row.currency}
|
||||
</p>
|
||||
{!isPaid && !isIncoming && row.type === 'invoice' && (
|
||||
<p className="text-[8px] text-slate-400 font-bold uppercase">
|
||||
Остаток после: {((currentBalance - row.amount) / 1000).toFixed(0)}k
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
{row.type === 'entry' && row.entryId && (
|
||||
<button
|
||||
onClick={() => handleDeleteEntry(row.entryId!)}
|
||||
className="p-2 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors"
|
||||
title="Удалить запись"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{row.type === 'invoice' &&
|
||||
(row.status !== 'paid' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePaymentActionClick(row, 'paid')}
|
||||
disabled={loading[row.paymentInvoiceId!]}
|
||||
className="p-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 disabled:opacity-50"
|
||||
title="Подтвердить оплату"
|
||||
>
|
||||
<Wallet className="w-4 h-4" />
|
||||
</button>
|
||||
{row.paymentInvoiceId && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePaymentActionClick(row, 'postponed')}
|
||||
disabled={loading[row.paymentInvoiceId!]}
|
||||
className="p-2 bg-orange-100 text-orange-600 rounded-lg hover:bg-orange-200 disabled:opacity-50"
|
||||
title="Отложить"
|
||||
>
|
||||
<Pause className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePaymentActionClick(row, 'cancelled')}
|
||||
disabled={loading[row.paymentInvoiceId!]}
|
||||
className="p-2 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 disabled:opacity-50"
|
||||
title="Отменить"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handlePaymentActionClick(row, 'postponed')}
|
||||
disabled={loading[row.paymentInvoiceId!]}
|
||||
className="p-2 bg-white text-slate-300 rounded-lg hover:text-indigo-600 disabled:opacity-50"
|
||||
title="Вернуть в план"
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEntryForm && (
|
||||
<PaymentCalendarEntryForm
|
||||
currentUserId={currentUserId}
|
||||
defaultDirection={defaultFormDirection}
|
||||
onSave={async () => {
|
||||
setShowEntryForm(false);
|
||||
onRefreshCalendar?.();
|
||||
}}
|
||||
onCancel={() => setShowEntryForm(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{statusModal && (
|
||||
<PaymentStatusModal
|
||||
action={statusModal.action}
|
||||
invoiceLabel={`${statusModal.row.contractorName} — ${statusModal.row.amount.toLocaleString('ru-RU')} ₽`}
|
||||
onConfirm={handleStatusModalConfirm}
|
||||
onCancel={() => setStatusModal(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user