102 lines
6.7 KiB
TypeScript
102 lines
6.7 KiB
TypeScript
|
|
|
|||
|
|
import React from 'react';
|
|||
|
|
import { Invoice, InvoiceStatus } from '../../types';
|
|||
|
|
import { FileText, FileSearch, AlertCircle, FileCheck, Check, X, MessageSquareQuote, CalendarPlus } from 'lucide-react';
|
|||
|
|
|
|||
|
|
const StatusBadge: React.FC<{ status: InvoiceStatus }> = ({ status }) => {
|
|||
|
|
const config: Record<InvoiceStatus, { label: string, color: string, bg: string }> = {
|
|||
|
|
draft: { label: 'Черновик', color: 'text-slate-500', bg: 'bg-slate-100' },
|
|||
|
|
pending_approval: { label: 'На согл.', color: 'text-amber-600', bg: 'bg-amber-50' },
|
|||
|
|
clarification: { label: 'Уточнение', color: 'text-purple-600', bg: 'bg-purple-50' },
|
|||
|
|
approved: { label: 'Согласован', color: 'text-blue-600', bg: 'bg-blue-50' },
|
|||
|
|
scheduled: { label: 'В графике', color: 'text-indigo-600', bg: 'bg-indigo-50' },
|
|||
|
|
paid: { label: 'Оплачен', color: 'text-emerald-600', bg: 'bg-emerald-50' },
|
|||
|
|
rejected: { label: 'Отказ', color: 'text-red-600', bg: 'bg-red-50' },
|
|||
|
|
overdue: { label: 'Просрочен', color: 'text-white', bg: 'bg-red-500' },
|
|||
|
|
};
|
|||
|
|
const s = config[status] || config.draft;
|
|||
|
|
return (
|
|||
|
|
<span className={`px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider ${s.bg} ${s.color}`}>
|
|||
|
|
{s.label}
|
|||
|
|
</span>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const InvoiceRegistry: React.FC<{ invoices: Invoice[], onUpdateStatus: (id: string, s: InvoiceStatus, extra?: any) => void }> = ({ invoices, onUpdateStatus }) => {
|
|||
|
|
// Show only "inbox" invoices: draft, pending, clarification, approved
|
|||
|
|
const registryInvoices = invoices.filter(i => ['draft', 'pending_approval', 'clarification', 'approved', 'rejected'].includes(i.status));
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden animate-fade-in">
|
|||
|
|
<div className="p-4 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
|
|||
|
|
<h3 className="font-black text-slate-700 text-[10px] uppercase tracking-widest">Реестр согласования счетов</h3>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button className="p-1.5 bg-white border border-slate-200 rounded-lg text-slate-400"><FileSearch className="w-4 h-4"/></button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="divide-y divide-slate-100">
|
|||
|
|
{registryInvoices.length === 0 && <div className="p-10 text-center text-slate-400 text-sm">Новых счетов нет</div>}
|
|||
|
|
{registryInvoices.map(inv => (
|
|||
|
|
<div key={inv.id} className="p-4 flex flex-col md:flex-row md:items-center justify-between hover:bg-slate-50 transition-colors group gap-4">
|
|||
|
|
<div className="flex gap-4 items-center flex-1">
|
|||
|
|
<div className={`p-3 rounded-xl ${inv.status === 'approved' ? 'bg-blue-50 text-blue-600' : 'bg-slate-50 text-slate-400'}`}>
|
|||
|
|
<FileText className="w-5 h-5"/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div className="flex items-center gap-2 mb-1">
|
|||
|
|
<StatusBadge status={inv.status}/>
|
|||
|
|
{inv.priority === 'high' && <span className="text-[9px] font-black text-red-500 uppercase">Срочно</span>}
|
|||
|
|
</div>
|
|||
|
|
<p className="text-sm font-bold text-slate-800">{inv.contractorName}</p>
|
|||
|
|
<p className="text-[10px] text-slate-500 font-medium">{inv.address} • {inv.serviceName}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between md:justify-end gap-6">
|
|||
|
|
<div className="text-right">
|
|||
|
|
<p className="text-sm font-black text-slate-900">{inv.amount.toLocaleString()} ₽</p>
|
|||
|
|
<p className="text-[10px] text-slate-400 font-medium">{inv.date}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
{inv.status === 'pending_approval' || inv.status === 'clarification' ? (
|
|||
|
|
<>
|
|||
|
|
<button
|
|||
|
|
onClick={() => onUpdateStatus(inv.id, 'approved')}
|
|||
|
|
className="p-2 bg-emerald-50 text-emerald-600 rounded-xl hover:bg-emerald-100 border border-emerald-100"
|
|||
|
|
title="Согласовать"
|
|||
|
|
>
|
|||
|
|
<Check className="w-4 h-4"/>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => onUpdateStatus(inv.id, 'clarification')}
|
|||
|
|
className="p-2 bg-purple-50 text-purple-600 rounded-xl hover:bg-purple-100 border border-purple-100"
|
|||
|
|
title="На уточнение"
|
|||
|
|
>
|
|||
|
|
<MessageSquareQuote className="w-4 h-4"/>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => onUpdateStatus(inv.id, 'rejected')}
|
|||
|
|
className="p-2 bg-red-50 text-red-600 rounded-xl hover:bg-red-100 border border-red-100"
|
|||
|
|
title="Отказать"
|
|||
|
|
>
|
|||
|
|
<X className="w-4 h-4"/>
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
) : inv.status === 'approved' ? (
|
|||
|
|
<button
|
|||
|
|
onClick={() => onUpdateStatus(inv.id, 'scheduled', { scheduledDate: new Date().toISOString().split('T')[0] })}
|
|||
|
|
className="px-3 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 text-[10px] font-black uppercase flex items-center gap-1.5 shadow-lg shadow-indigo-500/20"
|
|||
|
|
>
|
|||
|
|
<CalendarPlus className="w-4 h-4"/> В график
|
|||
|
|
</button>
|
|||
|
|
) : null}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|