diff --git a/src/App.jsx b/src/App.jsx index c955669..295d93f 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,6 +21,7 @@ const CreateInvoice = lazy(() => import('./pages/team/CreateInvoice')); const CreateSubcontractorPO = lazy(() => import('./pages/team/CreateSubcontractorPO')); const InvoiceDetail = lazy(() => import('./pages/team/InvoiceDetail')); const SubcontractorPODetail = lazy(() => import('./pages/team/SubcontractorPODetail')); +const SubInvoiceDetail = lazy(() => import('./pages/team/SubInvoiceDetail')); const SurveyMaker = lazy(() => import('./pages/team/SurveyMaker')); const BrandBook = lazy(() => import('./pages/team/BrandBook')); const Converters = lazy(() => import('./pages/team/Converters')); @@ -64,6 +65,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/team/Invoices.jsx b/src/pages/team/Invoices.jsx index 6a8b574..b865e43 100644 --- a/src/pages/team/Invoices.jsx +++ b/src/pages/team/Invoices.jsx @@ -430,7 +430,10 @@ export default function Invoices() { const payableSubcontractorPOs = subcontractorPOs.filter(po => ['approved', 'ready_to_pay'].includes(po.status)); const selectedSubcontractorPO = subcontractorPOs.find(po => po.id === selectedSubcontractorPOId); const totalPaidSubcontractors = paidSubcontractorPOs.reduce((s, po) => s + Number(po.amount), 0); - const totalPayableSubcontractors = payableSubcontractorPOs.reduce((s, po) => s + Number(po.amount), 0); + const totalPayableSubcontractorPOs = payableSubcontractorPOs.reduce((s, po) => s + Number(po.amount), 0); + const totalPayableSubInvoices = subInvoices.filter(i => i.status === 'submitted').reduce((s, i) => s + (i.items || []).reduce((a, x) => a + Number(x.unit_price || 0) * Number(x.quantity || 1), 0), 0); + const totalPayableSubcontractors = totalPayableSubcontractorPOs + totalPayableSubInvoices; + const payableSubcontractorCount = payableSubcontractorPOs.length + subInvoices.filter(i => i.status === 'submitted').length; const totalExpenses = filteredExpenses.reduce((s, e) => s + Number(e.amount), 0); const currentYear = new Date().getFullYear(); const yearExpenses = expenses.filter(e => new Date(e.date).getFullYear() === currentYear); @@ -494,7 +497,7 @@ export default function Invoices() {
${totalPayableSubcontractors.toFixed(2)}
-
Subcontractors Payable · {payableSubcontractorPOs.length} approved/ready
+
Subcontractors Payable · {payableSubcontractorCount} pending
@@ -860,82 +863,19 @@ export default function Invoices() { {subInvoices.map(inv => { const total = (inv.items || []).reduce((s, i) => s + Number(i.unit_price || 0) * Number(i.quantity || 1), 0); - const isExpanded = expandedSubInvoiceId === inv.id; const subInvoiceStatusColor = { draft: 'not_started', submitted: 'in_progress', paid: 'client_approved' }; return ( - <> - setExpandedSubInvoiceId(isExpanded ? null : inv.id)}> - {inv.invoice_number} - -
{inv.profile?.name || 'External'}
-
{inv.profile?.email || '—'}
- - {inv.submitted_at ? new Date(inv.submitted_at).toLocaleDateString() : } - {inv.status} - ${total.toFixed(2)} - -
e.stopPropagation()}> - {inv.status === 'submitted' && ( - - )} - {inv.status === 'paid' && ( - - )} -
- - - {isExpanded && ( - - - {inv.items?.length > 0 ? ( - - - - - - - - - - - {(inv.items || []).slice().sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)).map(item => ( - - - - - - - ))} - -
DescriptionQtyUnit PriceAmount
{item.description}{item.quantity}${Number(item.unit_price).toFixed(2)}${(Number(item.unit_price) * Number(item.quantity || 1)).toFixed(2)}
- ) : No line items.} - {inv.notes &&

{inv.notes}

} - {inv.paid_at &&

Paid {new Date(inv.paid_at).toLocaleDateString()}

} - - - )} - + navigate(`/sub-invoices/${inv.id}`)}> + {inv.invoice_number} + +
{inv.profile?.name || 'External'}
+
{inv.profile?.email || '—'}
+ + {inv.submitted_at ? new Date(inv.submitted_at).toLocaleDateString() : } + {inv.status} + ${total.toFixed(2)} + + ); })} diff --git a/src/pages/team/SubInvoiceDetail.jsx b/src/pages/team/SubInvoiceDetail.jsx new file mode 100644 index 0000000..9637146 --- /dev/null +++ b/src/pages/team/SubInvoiceDetail.jsx @@ -0,0 +1,200 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import Layout from '../../components/Layout'; +import { supabase } from '../../lib/supabase'; +import { generateSubcontractorPOPDF } from '../../lib/invoice'; +import { blobToEmailAttachment, sendEmail } from '../../lib/email'; + +const statusColor = { draft: 'not_started', submitted: 'in_progress', paid: 'client_approved' }; + +export default function SubInvoiceDetail() { + const { id } = useParams(); + const navigate = useNavigate(); + const [invoice, setInvoice] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [generating, setGenerating] = useState(false); + + useEffect(() => { + supabase + .from('subcontractor_invoices') + .select('*, profile:profiles!subcontractor_invoices_profile_id_fkey(id, name, email), items:subcontractor_invoice_items(*)') + .eq('id', id) + .single() + .then(({ data }) => { + setInvoice(data); + setLoading(false); + }); + }, [id]); + + const total = (invoice?.items || []).reduce((s, i) => s + Number(i.unit_price || 0) * Number(i.quantity || 1), 0); + + const buildPDFArgs = (inv) => ({ + po_number: inv.invoice_number, + status: 'paid', + profile: inv.profile, + project: { name: 'Subcontractor Invoice', company: { name: 'Fourge Branding' } }, + date: inv.created_at?.split('T')[0], + due_date: (inv.paid_at || new Date().toISOString()).split('T')[0], + amount: total, + description: 'Payment for completed subcontractor work.', + notes: inv.notes, + items: (inv.items || []).map((item, idx) => ({ + description: item.description, + amount: Number(item.unit_price) * Number(item.quantity || 1), + sort_order: item.sort_order ?? idx, + })), + }); + + const handleMarkPaid = async () => { + setSaving(true); + try { + const paidAt = new Date().toISOString(); + const { error } = await supabase.from('subcontractor_invoices').update({ status: 'paid', paid_at: paidAt }).eq('id', id); + if (error) throw error; + const updated = { ...invoice, status: 'paid', paid_at: paidAt }; + setInvoice(updated); + let pdfBlob = null; + try { pdfBlob = await generateSubcontractorPOPDF(buildPDFArgs(updated), { output: 'blob' }); } catch {} + if (invoice.profile?.email) { + try { + const attachments = pdfBlob ? [await blobToEmailAttachment(pdfBlob, `${invoice.invoice_number}-receipt.pdf`)] : []; + await sendEmail('receipt_sent', invoice.profile.email, { + invoiceNumber: invoice.invoice_number, + billTo: invoice.profile.name || invoice.profile.email, + total: total.toFixed(2), + paidDate: new Date(paidAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), + }, attachments); + } catch (emailErr) { + alert(`Marked paid, but email failed: ${emailErr.message || 'unknown error'}`); + } + } + } catch (err) { + alert(`Failed to mark as paid: ${err.message}`); + } + setSaving(false); + }; + + const handleReceipt = async () => { + setGenerating(true); + try { + await generateSubcontractorPOPDF(buildPDFArgs(invoice)); + } catch (err) { + alert(err.message); + } + setGenerating(false); + }; + + const handleDelete = async () => { + if (!window.confirm(`Delete invoice ${invoice.invoice_number}? This cannot be undone.`)) return; + setSaving(true); + const { error } = await supabase.from('subcontractor_invoices').delete().eq('id', id); + if (error) { alert('Failed to delete: ' + error.message); setSaving(false); return; } + navigate('/invoices', { state: { tab: 'sub-invoices' } }); + }; + + if (loading) return

Loading...

; + if (!invoice) return

Invoice not found.

; + + const sortedItems = [...(invoice.items || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)); + + return ( + + + +
+
+
{invoice.invoice_number}
+
+ {invoice.profile?.name || 'External'} · {invoice.profile?.email || '—'} +
+
+ + {invoice.status} + +
+ +
+
+
Invoice Info
+
+

{invoice.invoice_number}

+

{invoice.status}

+

{invoice.submitted_at ? new Date(invoice.submitted_at).toLocaleDateString() : '—'}

+ {invoice.paid_at && ( +

{new Date(invoice.paid_at).toLocaleDateString()}

+ )} +
+
+
+
Summary
+
+

{sortedItems.length}

+

${total.toFixed(2)}

+
+
+
+ +
+
Line Items
+ {sortedItems.length === 0 ? ( +

No line items.

+ ) : ( +
+ + + + + + + + + + + {sortedItems.map(item => ( + + + + + + + ))} + + + + + +
DescriptionQtyUnit PriceAmount
{item.description}{item.quantity}${Number(item.unit_price).toFixed(2)}${(Number(item.unit_price) * Number(item.quantity || 1)).toFixed(2)}
Total${total.toFixed(2)}
+
+ )} + {invoice.notes && ( +
+
Notes
+

{invoice.notes}

+
+ )} +
+ +
+
Actions
+
+ {invoice.status === 'submitted' && ( + + )} + {invoice.status === 'paid' && ( + + )} + +
+
+
+ ); +}