Add company tabs to client invoices page
Tabs match projects page style, sorted alphabetically. Stats (Outstanding, Paid, Overdue) update to reflect the selected company only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,15 @@ import Layout from '../../components/Layout';
|
|||||||
import LoadingButton from '../../components/LoadingButton';
|
import LoadingButton from '../../components/LoadingButton';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { generateInvoicePDF } from '../../lib/invoice';
|
import { generateInvoicePDF } from '../../lib/invoice';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' };
|
const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' };
|
||||||
|
|
||||||
export default function MyInvoices() {
|
export default function MyInvoices() {
|
||||||
|
const { currentUser } = useAuth();
|
||||||
|
const companies = (currentUser?.companies?.length ? currentUser.companies : (currentUser?.company ? [currentUser.company] : [])).slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const [activeCompanyId, setActiveCompanyId] = useState(companies[0]?.id || null);
|
||||||
|
|
||||||
const [invoices, setInvoices] = useState([]);
|
const [invoices, setInvoices] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [generatingInvoiceId, setGeneratingInvoiceId] = useState('');
|
const [generatingInvoiceId, setGeneratingInvoiceId] = useState('');
|
||||||
@@ -33,19 +38,44 @@ export default function MyInvoices() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const outstanding = invoices.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total), 0);
|
const visible = companies.length > 1 && activeCompanyId
|
||||||
const paid = invoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total), 0);
|
? invoices.filter(inv => inv.company_id === activeCompanyId)
|
||||||
const overdueCount = invoices.filter(inv => inv.status !== 'paid' && new Date(inv.due_date) < new Date()).length;
|
: invoices;
|
||||||
|
|
||||||
|
const outstanding = visible.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total), 0);
|
||||||
|
const paid = visible.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total), 0);
|
||||||
|
const overdueCount = visible.filter(inv => inv.status !== 'paid' && new Date(inv.due_date) < new Date()).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div>
|
<div>
|
||||||
<div className="page-title">Invoices</div>
|
<div className="page-title">Invoices</div>
|
||||||
<div className="page-subtitle">{invoices.length} invoice{invoices.length !== 1 ? 's' : ''}</div>
|
<div className="page-subtitle">{visible.length} invoice{visible.length !== 1 ? 's' : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{companies.length > 1 && (
|
||||||
|
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }} className="card-title">
|
||||||
|
{companies.map((company, index) => (
|
||||||
|
<span key={company.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
{index > 0 && <span style={{ color: 'var(--text-muted)' }}>|</span>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveCompanyId(company.id)}
|
||||||
|
style={{
|
||||||
|
background: 'none', border: 'none', padding: 0, margin: 0,
|
||||||
|
cursor: 'pointer', font: 'inherit', textTransform: 'inherit', letterSpacing: 'inherit',
|
||||||
|
color: activeCompanyId === company.id ? 'var(--text-primary)' : 'var(--text-muted)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{company.name}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="stats-grid" style={{ marginBottom: 24 }}>
|
<div className="stats-grid" style={{ marginBottom: 24 }}>
|
||||||
<div className="stat-card stat-card-highlight">
|
<div className="stat-card stat-card-highlight">
|
||||||
<div className="stat-value" style={{ fontSize: 22 }}>${outstanding.toFixed(2)}</div>
|
<div className="stat-value" style={{ fontSize: 22 }}>${outstanding.toFixed(2)}</div>
|
||||||
@@ -63,14 +93,14 @@ export default function MyInvoices() {
|
|||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
||||||
) : invoices.length === 0 ? (
|
) : visible.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<h3>No invoices yet</h3>
|
<h3>No invoices yet</h3>
|
||||||
<p>Your invoices will appear here once they are sent.</p>
|
<p>Your invoices will appear here once they are sent.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{invoices.map(inv => {
|
{visible.map(inv => {
|
||||||
const isOverdue = inv.status !== 'paid' && new Date(inv.due_date) < new Date();
|
const isOverdue = inv.status !== 'paid' && new Date(inv.due_date) < new Date();
|
||||||
return (
|
return (
|
||||||
<div key={inv.id} className="interactive-surface" style={{ border: '1px solid var(--border)', borderRadius: 8, background: 'var(--card-bg)', padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 16 }}>
|
<div key={inv.id} className="interactive-surface" style={{ border: '1px solid var(--border)', borderRadius: 8, background: 'var(--card-bg)', padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user