diff --git a/api/filebrowser.js b/api/filebrowser.js index 5825dcd..653f205 100644 --- a/api/filebrowser.js +++ b/api/filebrowser.js @@ -37,18 +37,6 @@ function safeName(value, fallback = '') { return cleaned || fallback; } -function parseBody(body) { - if (!body) return {}; - if (typeof body === 'string') { - try { - return JSON.parse(body); - } catch { - return {}; - } - } - return body; -} - async function readBody(req) { return new Promise((resolve, reject) => { const chunks = []; diff --git a/package-lock.json b/package-lock.json index a6a6ec3..07e1a46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@supabase/supabase-js": "^2.99.3", "heic-to": "^1.4.2", - "heic2any": "^0.0.4", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", "jszip": "^3.10.1", @@ -2026,12 +2025,6 @@ "integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==", "license": "LGPL-3.0" }, - "node_modules/heic2any": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz", - "integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==", - "license": "MIT" - }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", diff --git a/package.json b/package.json index 54648c8..d06bd90 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "dependencies": { "@supabase/supabase-js": "^2.99.3", "heic-to": "^1.4.2", - "heic2any": "^0.0.4", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", "jszip": "^3.10.1", diff --git a/src/App.jsx b/src/App.jsx index 51d5b20..85bb28c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ import { lazy, Suspense, Component } from 'react'; import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom'; -import { AuthProvider, useAuth } from './context/AuthContext'; +import { AuthProvider } from './context/AuthContext'; import ProtectedRoute from './components/ProtectedRoute'; import PageLoader from './components/PageLoader'; diff --git a/src/components/FilterDropdown.jsx b/src/components/FilterDropdown.jsx index 9be0a22..684417d 100644 --- a/src/components/FilterDropdown.jsx +++ b/src/components/FilterDropdown.jsx @@ -23,7 +23,6 @@ export default function FilterDropdown({ value, onChange, options }) { useEffect(() => { if (!open) return; - place(); function onDown(e) { if (btnRef.current?.contains(e.target) || menuRef.current?.contains(e.target)) return; setOpen(false); @@ -48,7 +47,7 @@ export default function FilterDropdown({ value, onChange, options }) { ; const filteredStatTasks = filteredRows.map(r => tasks.find(t => t.id === r.id)).filter(Boolean); - const filteredProjectsShell = filterCompany ? projects.filter(p => p.company_id === filterCompany) : projects; return ( - { - setAddProjectForm(f => ({ ...f, companyId: !isTeam && filterableCompanies.length === 1 ? filterableCompanies[0].id : f.companyId })); - setShowAddProjectForm(true); setAddProjectError(''); - } : null} - > + {/* Add project modal */} {showAddProjectForm && (
diff --git a/src/pages/client/ClientMyInvoices.jsx b/src/pages/client/ClientMyInvoices.jsx index 84ceb05..0cd18e0 100644 --- a/src/pages/client/ClientMyInvoices.jsx +++ b/src/pages/client/ClientMyInvoices.jsx @@ -10,9 +10,11 @@ import { supabase } from '../../lib/supabase'; import { generateInvoicePDF, generateReceiptPDF } from '../../lib/invoice'; import { useAuth } from '../../context/AuthContext'; import { useSortable } from '../../hooks/useSortable'; -import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; +import { POPUP_FIELD_LABEL } from '../../lib/popupStyles'; import InvoicePopupTable from '../../components/InvoicePopupTable'; +const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; const invoiceStatusLabel = (status) => { if (status === 'sent') return 'Invoiced'; @@ -165,7 +167,6 @@ export default function MyInvoices() { return () => document.removeEventListener('mousedown', onDocClick); }, [filterMenuOpen]); - const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const invoiceYears = [...new Set(invoices.map(i => i.invoice_date?.slice(0, 4)).filter(Boolean))].sort((a, b) => b.localeCompare(a)); const availableCompanyOptions = (currentUser?.companies?.length ? currentUser.companies : (currentUser?.company ? [currentUser.company] : [])) .filter(company => company?.id && invoices.some(inv => inv.company_id === company.id)) diff --git a/src/pages/external/ExternalMyInvoices.jsx b/src/pages/external/ExternalMyInvoices.jsx index 0dd855e..833f8d2 100644 --- a/src/pages/external/ExternalMyInvoices.jsx +++ b/src/pages/external/ExternalMyInvoices.jsx @@ -9,8 +9,8 @@ import { readPageCache, writePageCache } from '../../lib/pageCache'; import { useSortable } from '../../hooks/useSortable'; import { isCompletedVersionEligible } from '../../lib/invoiceVersionRules'; import SubcontractorInvoiceForm from '../../components/SubcontractorInvoiceForm'; -import { popupOverlayStyle, popupSurfaceStyle } from '../../lib/popupStyles'; -import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import { popupOverlayStyle, popupSurfaceStyle, POPUP_FIELD_LABEL } from '../../lib/popupStyles'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; import InvoicePopupTable from '../../components/InvoicePopupTable'; import { generateSubcontractorPOPDF } from '../../lib/invoice'; @@ -60,10 +60,6 @@ function parseItemVersionNumber(item) { return match ? Number(match[1]) : 0; } -function itemVersionLabel(item) { - return `R${String(parseItemVersionNumber(item)).padStart(2, '0')}`; -} - function itemWorkLabel(item) { return parseItemVersionNumber(item) > 0 ? 'Revision' : 'New'; } @@ -72,24 +68,6 @@ function cleanItemDescription(item) { return String(item?.description || '—').replace(/\s*[–-]\s*R\d{2}\b/i, '').trim() || '—'; } -async function fetchTaskTypeMap(taskIds) { - const uniqueTaskIds = [...new Set((taskIds || []).filter(Boolean))]; - if (uniqueTaskIds.length === 0) return {}; - const { data, error } = await supabase - .from('tasks') - .select('id, title, submissions(service_type, type)') - .in('id', uniqueTaskIds); - if (error) throw error; - const next = {}; - for (const task of (data || [])) { - const submissions = asArray(task.submissions); - const initial = submissions.find((submission) => submission?.type === 'initial' && submission?.service_type); - const fallback = submissions.find((submission) => submission?.service_type); - next[task.id] = initial?.service_type || fallback?.service_type || task.title || 'Other'; - } - return next; -} - function buildPDFArgs(invoice, profile, total, statusOverride) { return { po_number: invoice.invoice_number, @@ -125,7 +103,6 @@ export default function MyInvoices() { const [viewingInvoice, setViewingInvoice] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailError, setDetailError] = useState(''); - const [detailTaskTypeMap, setDetailTaskTypeMap] = useState({}); const [submittingInvoice, setSubmittingInvoice] = useState(false); const [downloadingInvoice, setDownloadingInvoice] = useState(false); const [downloadingReceipt, setDownloadingReceipt] = useState(false); @@ -249,8 +226,6 @@ export default function MyInvoices() { .eq('id', invoiceId) .single(); if (err || !data) throw err || new Error('Invoice not found.'); - const taskTypeMap = await fetchTaskTypeMap(asArray(data.items).map((item) => item.task_id)); - setDetailTaskTypeMap(taskTypeMap); setViewingInvoice(data); } catch (err) { setDetailError(err?.message || 'Failed to load invoice.'); diff --git a/src/pages/team/TeamDashboard.jsx b/src/pages/team/TeamDashboard.jsx index 915d6c8..5e066da 100644 --- a/src/pages/team/TeamDashboard.jsx +++ b/src/pages/team/TeamDashboard.jsx @@ -18,19 +18,6 @@ import { isReviewShadowDescription } from '../../lib/taskVersions'; // ─── Helpers ────────────────────────────────────────────────────────────── -const ICON_TONES = [ - { bg: 'color-mix(in srgb, var(--accent) 15%, transparent)', color: 'var(--accent)' }, - { bg: 'color-mix(in srgb, var(--positive) 15%, transparent)', color: 'var(--positive)' }, - { bg: 'color-mix(in srgb, var(--info) 15%, transparent)', color: 'var(--info)' }, - { bg: 'color-mix(in srgb, var(--violet) 15%, transparent)', color: 'var(--violet)' }, -]; - -function iconTone(key) { - let h = 0; - for (let i = 0; i < (key || '').length; i++) h = (h * 31 + key.charCodeAt(i)) % ICON_TONES.length; - return ICON_TONES[h]; -} - function fmtMoney(n) { if (Math.abs(n) >= 1000000) return `$${(n / 1000000).toFixed(2)}M`; if (Math.abs(n) >= 1000) return `$${(n / 1000).toFixed(1)}k`; @@ -486,7 +473,7 @@ function HotItemsCard({ submissions, tasks, isClient = false, isExternal = false ); } -function TeamPerformanceCard({ tasks, profiles, deliveries, submissions, cardHeight = null }) { +function TeamPerformanceCard({ profiles, deliveries, submissions, cardHeight = null }) { const navigate = useNavigate(); const profileMap = useMemo(() => { const m = new Map(); @@ -547,7 +534,7 @@ function TeamPerformanceCard({ tasks, profiles, deliveries, submissions, cardHei items.push({ task: e.task, kind: e.version === 0 ? 'new' : 'rev', monthKey, person }); }); return items; - }, [submissions, delByVer, delByTask, delAtVer, delAtTask]); // eslint-disable-line react-hooks/exhaustive-deps + }, [submissions, delByVer, delByTask, delAtVer, delAtTask]); const monthsWithData = useMemo(() => new Set(eligibleItems.map(i => i.monthKey).filter(Boolean)), [eligibleItems]); const allMonthOpts = useMemo(() => { @@ -869,7 +856,7 @@ export default function TeamDashboard() {
- +
); diff --git a/src/pages/team/TeamInvoiceDetail.jsx b/src/pages/team/TeamInvoiceDetail.jsx index 0b2fa78..87924e7 100644 --- a/src/pages/team/TeamInvoiceDetail.jsx +++ b/src/pages/team/TeamInvoiceDetail.jsx @@ -10,7 +10,8 @@ import { generateInvoicePDF, generateReceiptPDF } from '../../lib/invoice'; import { blobToEmailAttachment, sendEmail } from '../../lib/email'; import { withTimeout } from '../../lib/withTimeout'; import { useSortable } from '../../hooks/useSortable'; -import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; +import { POPUP_FIELD_LABEL } from '../../lib/popupStyles'; import InvoicePopupTable from '../../components/InvoicePopupTable'; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; diff --git a/src/pages/team/TeamInvoices.jsx b/src/pages/team/TeamInvoices.jsx index f051044..fc29302 100644 --- a/src/pages/team/TeamInvoices.jsx +++ b/src/pages/team/TeamInvoices.jsx @@ -11,36 +11,20 @@ import { useActionLock } from '../../hooks/useActionLock'; import { supabase } from '../../lib/supabase'; import { useAuth } from '../../context/AuthContext'; import { readPageCache, writePageCache } from '../../lib/pageCache'; -import { exportCPAPackage, generateSubcontractorPOPDF, generateInvoicePDF } from '../../lib/invoice'; +import { generateSubcontractorPOPDF, generateInvoicePDF } from '../../lib/invoice'; import { withTimeout } from '../../lib/withTimeout'; import LoadingButton from '../../components/LoadingButton'; import { blobToEmailAttachment, sendEmail } from '../../lib/email'; -import { popupOverlayStyle, popupSurfaceStyle } from '../../lib/popupStyles'; +import { popupOverlayStyle, popupSurfaceStyle, POPUP_FIELD_LABEL } from '../../lib/popupStyles'; import FileAttachment from '../../components/FileAttachment'; import { isReviewShadowDescription, pickInitialServiceType } from '../../lib/taskVersions'; import { getRevisionChargeQuantity, isCompletedVersionEligible, isInitialVersionEligible } from '../../lib/invoiceVersionRules'; import { TeamInvoiceDetailPanel } from './TeamInvoiceDetail'; -import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; import InvoicePopupTable from '../../components/InvoicePopupTable'; const CATEGORIES = ['Software', 'Contractor', 'Advertising', 'Office', 'Travel', 'Meals', 'Equipment', 'Other']; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; -const poStatusColor = { - draft: 'not_started', - sent: 'in_progress', - approved: 'client_approved', - ready_to_pay: 'in_progress', - paid: 'client_approved', - cancelled: 'needs_revision', -}; -const poStatusLabel = { - draft: 'Draft', - sent: 'Sent', - approved: 'Approved', - ready_to_pay: 'Ready to Pay', - paid: 'Paid', - cancelled: 'Cancelled', -}; const invoiceStatusBadgeLabel = (status) => { if (status === 'sent') return 'Invoiced'; return status ? status.charAt(0).toUpperCase() + status.slice(1) : '—'; @@ -126,10 +110,6 @@ function parseSubInvoiceItemVersionNumber(item) { return match ? Number(match[1]) : 0; } -function subInvoiceItemVersionLabel(item) { - return `R${String(parseSubInvoiceItemVersionNumber(item)).padStart(2, '0')}`; -} - function subInvoiceItemWorkLabel(item) { return parseSubInvoiceItemVersionNumber(item) > 0 ? 'Revision' : 'New'; } @@ -138,30 +118,6 @@ function cleanSubInvoiceItemDescription(item) { return String(item?.description || '—').replace(/\s*[–-]\s*R\d{2}\b/i, '').trim() || '—'; } -function asArray(value) { - if (Array.isArray(value)) return value; - if (value == null) return []; - return typeof value === 'object' ? [value] : []; -} - -async function fetchSubInvoiceTaskTypeMap(taskIds) { - const uniqueTaskIds = [...new Set((taskIds || []).filter(Boolean))]; - if (uniqueTaskIds.length === 0) return {}; - const { data, error } = await supabase - .from('tasks') - .select('id, title, submissions(service_type, type)') - .in('id', uniqueTaskIds); - if (error) throw error; - const next = {}; - for (const task of (data || [])) { - const submissions = asArray(task.submissions); - const initial = submissions.find((submission) => submission?.type === 'initial' && submission?.service_type); - const fallback = submissions.find((submission) => submission?.service_type); - next[task.id] = initial?.service_type || fallback?.service_type || task.title || 'Other'; - } - return next; -} - function CurrencyInput({ value, onChange, placeholder = '0.00', required = false, min = '0', step = '0.01' }) { return (
@@ -243,9 +199,6 @@ export default function Invoices() { const { sortKey: invSortKey, sortDir: invSortDir, toggle: invToggle, sort: invSort } = useSortable('invoice_date', 'desc'); const { sortKey: expSortKey, sortDir: expSortDir, toggle: expToggle, sort: expSort } = useSortable('date', 'desc'); const { sortKey: subSortKey, sortDir: subSortDir, toggle: subToggle, sort: subSort } = useSortable('submitted_at'); - const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date'); - const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date'); - const { sortKey: ovInvKey, sortDir: ovInvDir, toggle: ovInvToggle } = useSortable('invoice_date'); const { sortKey: subPopupKey, sortDir: subPopupDir, toggle: subPopupToggle, sort: subPopupSort } = useSortable('description'); const [filterCompany, setFilterCompany] = useState(''); const initMonth = () => { const n = new Date(); return { month: n.getMonth(), year: n.getFullYear() }; }; @@ -253,7 +206,6 @@ export default function Invoices() { const [ovMonth2, setOvMonth2] = useState(initMonth); const [ovMonth3, setOvMonth3] = useState(initMonth); const [exportYear, setExportYear] = useState(new Date().getFullYear()); - const [exporting, setExporting] = useState(false); const [expenses, setExpenses] = useState([]); const [expensesLoading, setExpensesLoading] = useState(true); @@ -272,18 +224,12 @@ export default function Invoices() { const [subWhoFilter, setSubWhoFilter] = useState('all'); const [subStatusFilter, setSubStatusFilter] = useState('all'); const [subYearFilter, setSubYearFilter] = useState('all'); - const [subcontractorPOs, setSubcontractorPOs] = useState([]); - const [subcontractorLoading, setSubcontractorLoading] = useState(true); - const [subcontractorError, setSubcontractorError] = useState(''); - const [selectedSubcontractorPOId, setSelectedSubcontractorPOId] = useState(''); const [subInvoices, setSubInvoices] = useState([]); const [subInvoicesLoading, setSubInvoicesLoading] = useState(true); const [subInvoicesError, setSubInvoicesError] = useState(''); const [markingPaid, setMarkingPaid] = useState(''); const [viewingSubInvoice, setViewingSubInvoice] = useState(null); - const [viewingSubInvoiceTaskTypeMap, setViewingSubInvoiceTaskTypeMap] = useState({}); const [viewingInvoice, setViewingInvoice] = useState(null); - const [expandedSubInvoiceId, setExpandedSubInvoiceId] = useState(null); // ── New Invoice form ────────────────────────────────────────────────────── const [showInvoiceForm, setShowInvoiceForm] = useState(false); @@ -301,30 +247,12 @@ export default function Invoices() { const guard = useActionLock(); const [invLoadingTasks, setInvLoadingTasks] = useState(false); const invDragItem = useRef(null); - const pageLoading = loading || expensesLoading || subcontractorLoading || subInvoicesLoading; + const pageLoading = loading || expensesLoading || subInvoicesLoading; useEffect(() => { supabase.from('companies').select('id, name, contact_email').order('name').then(({ data }) => setInvCompanies(data || [])); }, []); - useEffect(() => { - let cancelled = false; - async function loadSubInvoiceTaskTypes() { - if (!viewingSubInvoice?.items?.length) { - setViewingSubInvoiceTaskTypeMap({}); - return; - } - try { - const map = await fetchSubInvoiceTaskTypeMap(asArray(viewingSubInvoice.items).map((item) => item.task_id)); - if (!cancelled) setViewingSubInvoiceTaskTypeMap(map); - } catch { - if (!cancelled) setViewingSubInvoiceTaskTypeMap({}); - } - } - loadSubInvoiceTaskTypes(); - return () => { cancelled = true; }; - }, [viewingSubInvoice]); - useEffect(() => { if (!invCompanyId) { setInvUnbilledTasks([]); setInvUnbilledRevisions([]); setInvPriceList([]); setInvItems([invNewItem()]); setInvBillTo(''); setInvEmail(''); setInvRecipients([]); return; } const co = invCompanies.find(c => c.id === invCompanyId); @@ -456,13 +384,7 @@ export default function Invoices() { useEffect(() => { async function loadExpenses() { - const [{ data, error }, { data: purchaseOrders, error: purchaseOrdersError }] = await Promise.all([ - supabase.from('expenses').select('*').order('date', { ascending: false }), - supabase - .from('subcontractor_payments') - .select('*, profile:profiles!subcontractor_payments_profile_id_fkey(id, name, email), project:projects(id, name, company:companies(name)), items:subcontractor_po_items(*, task:tasks(id, title))') - .order('date', { ascending: false }), - ]); + const { data, error } = await supabase.from('expenses').select('*').order('date', { ascending: false }); if (error) { console.error('Failed to load expenses:', error); setExpensesError(error.message || 'Failed to load expenses.'); @@ -471,16 +393,7 @@ export default function Invoices() { setExpenses(data || []); setExpensesError(''); } - if (purchaseOrdersError) { - console.error('Failed to load subcontractor POs:', purchaseOrdersError); - setSubcontractorError(purchaseOrdersError.message || 'Failed to load subcontractor POs.'); - setSubcontractorPOs([]); - } else { - setSubcontractorPOs(purchaseOrders || []); - setSubcontractorError(''); - } setExpensesLoading(false); - setSubcontractorLoading(false); } loadExpenses(); }, []); @@ -579,37 +492,6 @@ export default function Invoices() { win.location.href = data.signedUrl; }; - const updateSubcontractorPO = async (po, updates, errorLabel = 'Failed to update PO') => { - const { data, error } = await supabase - .from('subcontractor_payments') - .update(updates) - .eq('id', po.id) - .select('*, profile:profiles!subcontractor_payments_profile_id_fkey(id, name, email), project:projects(id, name, company:companies(name)), items:subcontractor_po_items(*, task:tasks(id, title))') - .single(); - if (error) { - alert(`${errorLabel}: ${error.message}`); - return null; - } - if (data) { - setSubcontractorPOs(prev => prev.map(row => row.id === po.id ? data : row)); - setSelectedSubcontractorPOId(current => current === po.id ? data.id : current); - } - return data; - }; - - const handleSendSubcontractorPO = async (po) => { - await updateSubcontractorPO(po, { status: 'sent', sent_at: new Date().toISOString() }, 'Failed to send PO'); - }; - - const handleReadyToPaySubcontractorPO = (po) => { - updateSubcontractorPO(po, { status: 'ready_to_pay' }, 'Failed to mark ready to pay'); - }; - - const handleMarkSubcontractorPaid = async (po) => { - const paidAt = new Date().toISOString().slice(0, 10); - updateSubcontractorPO(po, { status: 'paid', paid_at: paidAt }, 'Failed to mark as paid'); - }; - const handleMarkSubInvoicePaid = async (invoice) => { setMarkingPaid(invoice.id); try { @@ -710,68 +592,6 @@ export default function Invoices() { } }; - const handleReopenSubcontractorPO = async (po) => { - updateSubcontractorPO(po, { status: 'draft', paid_at: null, cancelled_at: null }, 'Failed to reopen PO'); - }; - - const handleCancelSubcontractorPO = async (po) => { - if (!window.confirm(`Cancel ${po.po_number || 'this PO'}?`)) return; - updateSubcontractorPO(po, { status: 'cancelled', cancelled_at: new Date().toISOString() }, 'Failed to cancel PO'); - }; - - const handleDeleteSubcontractorPO = async (id) => { - if (!window.confirm('Delete this subcontractor PO?')) return; - await supabase.from('subcontractor_payments').delete().eq('id', id); - setSubcontractorPOs(prev => prev.filter(po => po.id !== id)); - setSelectedSubcontractorPOId(current => current === id ? '' : current); - }; - - const handleDownloadSubcontractorPO = async (po) => { - try { - await generateSubcontractorPOPDF(po); - } catch (error) { - console.error('Failed to download subcontractor PO:', error); - alert(`Failed to download PO: ${error.message || 'unknown error'}`); - } - }; - - const handleCPAExport = async () => { - setExporting(true); - try { - const yearInvoices = invoices.filter(inv => - inv.status === 'paid' && new Date(inv.invoice_date).getFullYear() === exportYear - ); - const yearExpenses = expenses.filter(exp => - new Date(exp.date).getFullYear() === exportYear - ); - const yearSubcontractorExpenses = subcontractorPOs - .filter(po => po.status === 'paid' && new Date(po.paid_at || po.date).getFullYear() === exportYear) - .map(po => ({ - date: po.paid_at || po.date, - category: 'Contractor', - description: `Subcontractor: ${po.profile?.name || 'External'} — ${po.items?.length ? po.items.map(item => item.description).join('; ') : po.description}`, - amount: po.amount, - notes: [po.po_number, po.project?.name, po.notes || po.profile?.email || ''].filter(Boolean).join(' · '), - })); - const exportExpenses = [...yearExpenses, ...yearSubcontractorExpenses]; - if (!yearInvoices.length && !exportExpenses.length) { - alert(`No data found for ${exportYear}.`); - return; - } - const { data: allItems } = yearInvoices.length ? await supabase - .from('invoice_items') - .select('invoice_id, description') - .in('invoice_id', yearInvoices.map(i => i.id)) : { data: [] }; - const itemsByInvoice = (allItems || []).reduce((acc, item) => { - (acc[item.invoice_id] = acc[item.invoice_id] || []).push(item); - return acc; - }, {}); - await exportCPAPackage(yearInvoices, itemsByInvoice, exportExpenses, exportYear); - } finally { - setExporting(false); - } - }; - const paidYears = [...new Set( invoices.filter(i => i.status === 'paid').map(i => new Date(i.invoice_date).getFullYear()) )].sort((a, b) => b - a); @@ -783,45 +603,12 @@ export default function Invoices() { return true; }); - const totals = { - all: invoices.reduce((s, i) => s + Number(i.total), 0), - draft: invoices.filter(i => i.status === 'draft').reduce((s, i) => s + Number(i.total), 0), - sent: invoices.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total), 0), - paid: invoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total), 0), - netReceived: invoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total) - Number(i.stripe_fee || 0), 0), - }; - const expenseYears = [...new Set(expenses.map(e => e.date?.slice(0, 4)).filter(Boolean))].sort((a, b) => b - a); const filteredExpenses = expenses.filter(e => { if (expYearFilter !== 'all' && e.date?.slice(0, 4) !== expYearFilter) return false; if (expenseFilter !== 'all' && e.category !== expenseFilter) return false; return true; }); - const paidSubcontractorPOs = subcontractorPOs.filter(po => po.status === 'paid'); - const payableSubcontractorPOs = subcontractorPOs.filter(po => ['approved', 'ready_to_pay'].includes(po.status)); - const selectedSubcontractorPO = subcontractorPOs.find(po => po.id === selectedSubcontractorPOId); - const subInvoiceItemTotal = (inv) => (inv.items || []).reduce((a, x) => a + Number(x.unit_price || 0) * Number(x.quantity || 1), 0); - const paidSubInvoices = subInvoices.filter(i => i.status === 'paid'); - const totalPaidSubInvoices = paidSubInvoices.reduce((s, i) => s + subInvoiceItemTotal(i), 0); - const totalPaidSubcontractors = paidSubcontractorPOs.reduce((s, po) => s + Number(po.amount), 0) + totalPaidSubInvoices; - const totalPayableSubcontractorPOs = payableSubcontractorPOs.reduce((s, po) => s + Number(po.amount), 0); - const totalPayableSubInvoices = subInvoices.filter(i => i.status === 'submitted').reduce((s, i) => s + subInvoiceItemTotal(i), 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); - const currentYearExpenseTotal = yearExpenses.reduce((s, e) => s + Number(e.amount), 0); - const currentYearPaidSubcontractorPOs = paidSubcontractorPOs - .filter(po => new Date(po.paid_at || po.date).getFullYear() === currentYear) - .reduce((s, po) => s + Number(po.amount), 0); - const currentYearPaidSubInvoices = paidSubInvoices - .filter(i => new Date(i.paid_at).getFullYear() === currentYear) - .reduce((s, i) => s + subInvoiceItemTotal(i), 0); - const currentYearTotalExpenses = currentYearExpenseTotal + currentYearPaidSubcontractorPOs + currentYearPaidSubInvoices; - const revenue = totals.paid; - const profit = totals.netReceived - expenses.reduce((s, e) => s + Number(e.amount), 0) - totalPaidSubcontractors; - const chartYear = exportYear; const chartData = useMemo(() => MONTHS.map((month, mi) => { const paidInvs = invoices.filter(inv => inv.status === 'paid' && new Date(inv.invoice_date).getFullYear() === chartYear && new Date(inv.invoice_date).getMonth() === mi); @@ -937,13 +724,6 @@ export default function Invoices() { const CARD = { background: 'var(--card-bg)', border: 'var(--card-border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' }; const TH = { fontSize: 10, fontWeight: 500, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6, padding: '0 0 12px 0', border: 'none', background: 'transparent', textAlign: 'left' }; const TD = { fontSize: 13, color: 'var(--text-primary)', padding: '5px 0', border: 'none', background: 'transparent', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; - const sortRows = (arr, key, dir, getter) => [...arr].sort((a, b) => { - const av = getter(a, key), bv = getter(b, key); - if (av < bv) return dir === 'asc' ? -1 : 1; - if (av > bv) return dir === 'asc' ? 1 : -1; - return 0; - }); - const fmtDate = d => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—'; const fmtAmt = v => `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const now = new Date(); @@ -1508,7 +1288,7 @@ export default function Invoices() { { id: 'invoices', label: 'INVOICES' }, { id: 'expenses', label: 'EXPENSES' }, { id: 'sub-invoices', label: 'SUBCONTRACTOR INVOICES' }, - ].map((t, i, arr) => ( + ].map((t) => (