import { useState, useEffect, useRef, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import Layout from '../../components/Layout'; import LoadingButton from '../../components/LoadingButton'; import { supabase } from '../../lib/supabase'; import { useAuth } from '../../context/AuthContext'; import { sendEmail } from '../../lib/email'; const INVOICE_TODAY = new Date().toISOString().split('T')[0]; function newItem(description = '', unit_price = '', quantity = 1, taskId = null, isRevision = false) { return { id: crypto.randomUUID(), description, unit_price, quantity, task_id: taskId, is_revision: isRevision }; } function genNumber(count) { const year = new Date().getFullYear(); return `INVSUB-${year}-${String(count + 1).padStart(3, '0')}`; } export default function MyInvoiceCreate() { const navigate = useNavigate(); const { currentUser } = useAuth(); const [invoiceNumber, setInvoiceNumber] = useState(''); const [completedTasks, setCompletedTasks] = useState([]); const [loadingTasks, setLoadingTasks] = useState(true); const [items, setItems] = useState([newItem()]); const [notes, setNotes] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const dragItem = useRef(null); const rate = Number(currentUser?.brand_book_rate || 60); useEffect(() => { async function load() { const [{ data: tasks }, { data: existingInvoices }, { data: nextNum }] = await Promise.all([ supabase .from('tasks') .select('id, title, current_version, project:projects(name)') .eq('status', 'client_approved') .eq('assigned_to', currentUser.id) .order('title'), supabase .from('subcontractor_invoices') .select('id, status, items:subcontractor_invoice_items(task_id)') .in('status', ['submitted', 'paid']), supabase.rpc('get_next_sub_invoice_number'), ]); setInvoiceNumber(nextNum || genNumber(0)); const invoicedTaskIds = new Set( (existingInvoices || []).flatMap(inv => (inv.items || []).map(i => i.task_id).filter(Boolean)) ); setCompletedTasks((tasks || []).filter(t => !invoicedTaskIds.has(t.id))); setLoadingTasks(false); } load(); }, []); const addedTaskIds = useMemo(() => new Set(items.map(i => i.task_id).filter(Boolean)), [items]); const addTask = (task) => { if (addedTaskIds.has(task.id)) return; const version = task.current_version || 0; const baseDesc = task.project?.name ? `${task.project.name} • ${task.title}` : task.title; const toAdd = [newItem(`${baseDesc} – R00`, rate, 1, task.id, false)]; for (let v = 1; v <= version; v++) { toAdd.push(newItem(`${baseDesc} – R${String(v).padStart(2, '0')}`, 30, 1, task.id, true)); } setItems(prev => { if (prev.length === 1 && !prev[0].description && !prev[0].unit_price) return toAdd; return [...prev, ...toAdd]; }); }; const updateItem = (id, field, value) => { setItems(prev => prev.map(item => item.id === id ? { ...item, [field]: value } : item)); }; const removeItem = (id) => setItems(prev => prev.filter(item => item.id !== id)); const handleDrop = (targetIndex) => { if (dragItem.current === null || dragItem.current === targetIndex) { dragItem.current = null; return; } setItems(prev => { const next = [...prev]; const [moved] = next.splice(dragItem.current, 1); next.splice(targetIndex, 0, moved); return next; }); dragItem.current = null; }; const total = items.reduce((s, i) => s + (Number(i.quantity) || 0) * (Number(i.unit_price) || 0), 0); const handleSubmit = async () => { const valid = items.filter(i => String(i.description).trim()); if (!valid.length) { setError('Add at least one line item.'); return; } setSaving(true); setError(''); try { const { data: inv, error: invErr } = await supabase .from('subcontractor_invoices') .insert({ profile_id: currentUser.id, invoice_number: invoiceNumber.trim(), status: 'submitted', notes: notes.trim(), submitted_at: new Date().toISOString(), }) .select() .single(); if (invErr) throw invErr; const { error: itemsErr } = await supabase.from('subcontractor_invoice_items').insert( valid.map((item, idx) => ({ invoice_id: inv.id, task_id: item.task_id || null, description: String(item.description).trim(), quantity: Number(item.quantity) || 1, unit_price: Number(item.unit_price) || 0, sort_order: idx, })) ); if (itemsErr) throw itemsErr; const total = valid.reduce((s, i) => s + (Number(i.unit_price) || 0) * (Number(i.quantity) || 1), 0); sendEmail('subcontractor_invoice_submitted', 'hello@fourgebranding.com', { subName: currentUser.name, invoiceNumber: inv.invoice_number, total: total.toFixed(2), invoiceId: inv.id, }).catch(err => console.error('Sub invoice notification failed:', err)); navigate(`/my-invoices-sub/${inv.id}`); } catch (err) { setError(err.message); setSaving(false); } }; return (
New Invoice
Invoice date: {new Date(INVOICE_TODAY).toLocaleDateString()} · {invoiceNumber}
{error &&
{error}
}
Completed Tasks
{completedTasks.length > 0 && !loadingTasks && ( )}
{loadingTasks ? (

Loading...

) : completedTasks.length === 0 ? (

No completed tasks available to invoice.

) : (
{completedTasks.map(task => { const alreadyAdded = addedTaskIds.has(task.id); return (
{task.project?.name ? `${task.project.name} • ` : ''}{task.title}
{(() => { const v = task.current_version || 0; const parts = [`R00 New Book $${rate.toFixed(2)}`]; if (v > 0) parts.push(`+ ${v} Revision${v > 1 ? 's' : ''} @ $30ea`); return parts.join(' · '); })()}
); })}
)}
Line Items
{['', 'Type', 'Description', 'Qty / Hrs', 'Rate', 'Total', ''].map((h, i) => (
3 ? 'right' : 'left' }}>{h}
))}
{items.map((item, index) => (
e.preventDefault()} onDrop={() => handleDrop(index)} style={{ display: 'grid', gridTemplateColumns: '20px 90px 1fr 80px 120px 120px 40px', gap: 8, alignItems: 'center' }} >
{ dragItem.current = index; e.dataTransfer.effectAllowed = 'move'; }} style={{ cursor: 'grab', color: 'var(--text-muted)', fontSize: 14, textAlign: 'center', userSelect: 'none' }} >⠿
{item.is_revision ? 'Revision' : 'New'} updateItem(item.id, 'description', e.target.value)} style={{ margin: 0 }} /> updateItem(item.id, 'quantity', e.target.value)} style={{ margin: 0, textAlign: 'center' }} /> updateItem(item.id, 'unit_price', e.target.value)} style={{ margin: 0, textAlign: 'right' }} />
${((Number(item.quantity) || 0) * (Number(item.unit_price) || 0)).toFixed(2)}
))}
Total
${total.toFixed(2)}
Notes