Files
limitless-ui/src/pages/Invoice.tsx
Claude 7eb18b15b8 fix(rsc): add "use client" directive to all client-interactive files
50 .tsx components and 6 .ts hook files use React client APIs
(createContext, useState, useEffect, useRef, useMemo, useCallback,
forwardRef, etc.) but lacked the "use client" directive at the top
of the source file. tsc-emitted dist files therefore also lacked it.

Older Next.js / Turbopack versions traversed the import graph and
implicitly treated the imports as client when their consumer was a
client component. Next.js 16's stricter Turbopack rejects this: any
file that touches client-only React APIs must declare "use client"
explicitly, regardless of where it is imported from.

Symptom in consumers (gscSupport build, 2026-05-17):

  ./templates/limitless-ui/dist/theme/ThemeProvider.js:2:10
  You're importing a module that depends on `createContext` into a
  React Server Component module. This API is only available in
  Client Components. To fix, mark the file (or its parent) with
  the `"use client"` directive.

Adding the directive at source-file level cascades through tsc into
the emitted dist/ — verified gscSupport + gscCRM build cleanly after
this change.

Affected files (50 total):
  src/theme/ThemeProvider.tsx
  src/hooks/useDisclosure.ts
  src/components/{Accordion,Carousel,DualListBox,Form,Wizard,…}.tsx
  src/validation/hooks/{useValidation,useFieldValidation,…}.ts
  src/genui/hooks/{useGenUI,useWebMCP}.ts
  ... and more — every component that touches createContext / a
  React hook now self-declares as client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:36:40 +02:00

1013 lines
32 KiB
TypeScript

"use client";
import React, { useState } from 'react';
// Types
export interface InvoiceItem {
id: string;
description: string;
quantity: number;
unitPrice: number;
total: number;
tax?: number;
}
export interface InvoiceData {
id: string;
invoiceNumber: string;
status: 'draft' | 'pending' | 'paid' | 'overdue' | 'cancelled';
issueDate: Date;
dueDate: Date;
paidDate?: Date;
from: {
name: string;
address?: string;
email?: string;
phone?: string;
logo?: string;
taxId?: string;
};
to: {
name: string;
address?: string;
email?: string;
phone?: string;
taxId?: string;
};
items: InvoiceItem[];
subtotal: number;
tax?: number;
taxRate?: number;
discount?: number;
discountType?: 'fixed' | 'percentage';
total: number;
currency?: string;
notes?: string;
terms?: string;
paymentMethod?: string;
}
// Invoice Template
export interface InvoiceTemplateProps {
/** Invoice data */
invoice: InvoiceData;
/** Print handler */
onPrint?: () => void;
/** Download handler */
onDownload?: () => void;
/** Send handler */
onSend?: () => void;
/** Edit handler */
onEdit?: () => void;
/** Additional CSS classes */
className?: string;
}
export const InvoiceTemplate: React.FC<InvoiceTemplateProps> = ({
invoice,
onPrint,
onDownload,
onSend,
onEdit,
className = '',
}) => {
const currency = invoice.currency || '$';
const formatCurrency = (amount: number) => {
return `${currency}${amount.toFixed(2)}`;
};
const formatDate = (date: Date) => {
return date.toLocaleDateString([], {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const getStatusColor = (status: InvoiceData['status']) => {
switch (status) {
case 'paid':
return '#10b981';
case 'pending':
return '#f59e0b';
case 'overdue':
return '#ef4444';
case 'draft':
return '#6b7280';
case 'cancelled':
return '#9ca3af';
default:
return '#6b7280';
}
};
return (
<div className={`ll-invoice-template ${className}`}>
<div className="ll-invoice-actions">
{onEdit && (
<button className="ll-invoice-action-btn" onClick={onEdit}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</svg>
Edit
</button>
)}
{onPrint && (
<button className="ll-invoice-action-btn" onClick={onPrint}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z" />
</svg>
Print
</button>
)}
{onDownload && (
<button className="ll-invoice-action-btn" onClick={onDownload}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
</svg>
Download
</button>
)}
{onSend && (
<button className="ll-invoice-action-btn ll-invoice-action-btn-primary" onClick={onSend}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
</svg>
Send
</button>
)}
</div>
<div className="ll-invoice-document">
<div className="ll-invoice-header">
<div className="ll-invoice-brand">
{invoice.from.logo ? (
<img src={invoice.from.logo} alt={invoice.from.name} className="ll-invoice-logo" />
) : (
<h1 className="ll-invoice-company">{invoice.from.name}</h1>
)}
</div>
<div className="ll-invoice-meta">
<h2 className="ll-invoice-title">INVOICE</h2>
<div className="ll-invoice-number">#{invoice.invoiceNumber}</div>
<div
className="ll-invoice-status"
style={{ backgroundColor: getStatusColor(invoice.status) }}
>
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</div>
</div>
</div>
<div className="ll-invoice-parties">
<div className="ll-invoice-from">
<h3>From</h3>
<p className="ll-invoice-party-name">{invoice.from.name}</p>
{invoice.from.address && <p>{invoice.from.address}</p>}
{invoice.from.email && <p>{invoice.from.email}</p>}
{invoice.from.phone && <p>{invoice.from.phone}</p>}
{invoice.from.taxId && <p>Tax ID: {invoice.from.taxId}</p>}
</div>
<div className="ll-invoice-to">
<h3>Bill To</h3>
<p className="ll-invoice-party-name">{invoice.to.name}</p>
{invoice.to.address && <p>{invoice.to.address}</p>}
{invoice.to.email && <p>{invoice.to.email}</p>}
{invoice.to.phone && <p>{invoice.to.phone}</p>}
{invoice.to.taxId && <p>Tax ID: {invoice.to.taxId}</p>}
</div>
<div className="ll-invoice-dates">
<div className="ll-invoice-date">
<span className="ll-invoice-date-label">Issue Date</span>
<span className="ll-invoice-date-value">{formatDate(invoice.issueDate)}</span>
</div>
<div className="ll-invoice-date">
<span className="ll-invoice-date-label">Due Date</span>
<span className="ll-invoice-date-value">{formatDate(invoice.dueDate)}</span>
</div>
{invoice.paidDate && (
<div className="ll-invoice-date">
<span className="ll-invoice-date-label">Paid Date</span>
<span className="ll-invoice-date-value">{formatDate(invoice.paidDate)}</span>
</div>
)}
</div>
</div>
<table className="ll-invoice-items">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
{invoice.items.some((item) => item.tax !== undefined) && <th>Tax</th>}
<th>Total</th>
</tr>
</thead>
<tbody>
{invoice.items.map((item) => (
<tr key={item.id}>
<td>{item.description}</td>
<td>{item.quantity}</td>
<td>{formatCurrency(item.unitPrice)}</td>
{invoice.items.some((i) => i.tax !== undefined) && (
<td>{item.tax !== undefined ? `${item.tax}%` : '-'}</td>
)}
<td>{formatCurrency(item.total)}</td>
</tr>
))}
</tbody>
</table>
<div className="ll-invoice-summary">
<div className="ll-invoice-notes">
{invoice.notes && (
<div className="ll-invoice-note">
<h4>Notes</h4>
<p>{invoice.notes}</p>
</div>
)}
{invoice.terms && (
<div className="ll-invoice-terms">
<h4>Terms & Conditions</h4>
<p>{invoice.terms}</p>
</div>
)}
{invoice.paymentMethod && (
<div className="ll-invoice-payment">
<h4>Payment Method</h4>
<p>{invoice.paymentMethod}</p>
</div>
)}
</div>
<div className="ll-invoice-totals">
<div className="ll-invoice-total-row">
<span>Subtotal</span>
<span>{formatCurrency(invoice.subtotal)}</span>
</div>
{invoice.discount !== undefined && invoice.discount > 0 && (
<div className="ll-invoice-total-row">
<span>
Discount
{invoice.discountType === 'percentage' && ` (${invoice.discount}%)`}
</span>
<span>
-{formatCurrency(
invoice.discountType === 'percentage'
? invoice.subtotal * (invoice.discount / 100)
: invoice.discount
)}
</span>
</div>
)}
{invoice.tax !== undefined && (
<div className="ll-invoice-total-row">
<span>Tax{invoice.taxRate && ` (${invoice.taxRate}%)`}</span>
<span>{formatCurrency(invoice.tax)}</span>
</div>
)}
<div className="ll-invoice-total-row ll-invoice-grand-total">
<span>Total</span>
<span>{formatCurrency(invoice.total)}</span>
</div>
</div>
</div>
</div>
</div>
);
};
// Invoice List
export interface InvoiceListProps {
/** Invoices to display */
invoices: InvoiceData[];
/** Invoice click handler */
onInvoiceClick?: (invoice: InvoiceData) => void;
/** Selection mode */
selectable?: boolean;
/** Selected IDs */
selectedIds?: string[];
/** Selection change handler */
onSelectionChange?: (ids: string[]) => void;
/** Sort field */
sortBy?: 'date' | 'number' | 'client' | 'amount' | 'status';
/** Sort direction */
sortDirection?: 'asc' | 'desc';
/** Sort change handler */
onSortChange?: (field: string, direction: 'asc' | 'desc') => void;
/** Loading state */
loading?: boolean;
/** Additional CSS classes */
className?: string;
}
export const InvoiceList: React.FC<InvoiceListProps> = ({
invoices,
onInvoiceClick,
selectable = false,
selectedIds = [],
onSelectionChange,
sortBy = 'date',
sortDirection = 'desc',
onSortChange,
loading = false,
className = '',
}) => {
const currency = '$';
const formatCurrency = (amount: number) => {
return `${currency}${amount.toFixed(2)}`;
};
const formatDate = (date: Date) => {
return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
};
const getStatusClass = (status: InvoiceData['status']) => {
return `ll-invoice-list-status-${status}`;
};
const handleSelectAll = () => {
if (selectedIds.length === invoices.length) {
onSelectionChange?.([]);
} else {
onSelectionChange?.(invoices.map((i) => i.id));
}
};
const handleSelect = (id: string) => {
if (selectedIds.includes(id)) {
onSelectionChange?.(selectedIds.filter((sid) => sid !== id));
} else {
onSelectionChange?.([...selectedIds, id]);
}
};
const handleSort = (field: string) => {
const newDirection = sortBy === field && sortDirection === 'asc' ? 'desc' : 'asc';
onSortChange?.(field, newDirection);
};
const SortIcon = ({ field }: { field: string }) => {
if (sortBy !== field) return null;
return (
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" className="ll-invoice-sort-icon">
<path d={sortDirection === 'asc' ? 'M7 14l5-5 5 5z' : 'M7 10l5 5 5-5z'} />
</svg>
);
};
if (loading) {
return (
<div className={`ll-invoice-list ll-invoice-list-loading ${className}`}>
<div className="ll-invoice-list-spinner" />
</div>
);
}
return (
<div className={`ll-invoice-list ${className}`}>
<table className="ll-invoice-list-table">
<thead>
<tr>
{selectable && (
<th className="ll-invoice-list-checkbox">
<input
type="checkbox"
checked={selectedIds.length === invoices.length && invoices.length > 0}
onChange={handleSelectAll}
/>
</th>
)}
<th onClick={() => handleSort('number')} className="ll-invoice-list-sortable">
Invoice <SortIcon field="number" />
</th>
<th onClick={() => handleSort('client')} className="ll-invoice-list-sortable">
Client <SortIcon field="client" />
</th>
<th onClick={() => handleSort('date')} className="ll-invoice-list-sortable">
Date <SortIcon field="date" />
</th>
<th>Due Date</th>
<th onClick={() => handleSort('amount')} className="ll-invoice-list-sortable">
Amount <SortIcon field="amount" />
</th>
<th onClick={() => handleSort('status')} className="ll-invoice-list-sortable">
Status <SortIcon field="status" />
</th>
</tr>
</thead>
<tbody>
{invoices.map((invoice) => (
<tr
key={invoice.id}
className={selectedIds.includes(invoice.id) ? 'll-invoice-list-selected' : ''}
onClick={() => onInvoiceClick?.(invoice)}
>
{selectable && (
<td className="ll-invoice-list-checkbox" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={selectedIds.includes(invoice.id)}
onChange={() => handleSelect(invoice.id)}
/>
</td>
)}
<td className="ll-invoice-list-number">#{invoice.invoiceNumber}</td>
<td className="ll-invoice-list-client">{invoice.to.name}</td>
<td className="ll-invoice-list-date">{formatDate(invoice.issueDate)}</td>
<td className="ll-invoice-list-due">{formatDate(invoice.dueDate)}</td>
<td className="ll-invoice-list-amount">{formatCurrency(invoice.total)}</td>
<td>
<span className={`ll-invoice-list-status ${getStatusClass(invoice.status)}`}>
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</span>
</td>
</tr>
))}
</tbody>
</table>
{invoices.length === 0 && (
<div className="ll-invoice-list-empty">
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" />
</svg>
<p>No invoices found</p>
</div>
)}
</div>
);
};
// Invoice Grid
export interface InvoiceGridProps {
/** Invoices to display */
invoices: InvoiceData[];
/** Invoice click handler */
onInvoiceClick?: (invoice: InvoiceData) => void;
/** Loading state */
loading?: boolean;
/** Additional CSS classes */
className?: string;
}
export const InvoiceGrid: React.FC<InvoiceGridProps> = ({
invoices,
onInvoiceClick,
loading = false,
className = '',
}) => {
const currency = '$';
const formatCurrency = (amount: number) => {
return `${currency}${amount.toFixed(2)}`;
};
const formatDate = (date: Date) => {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
};
const getStatusColor = (status: InvoiceData['status']) => {
switch (status) {
case 'paid': return '#10b981';
case 'pending': return '#f59e0b';
case 'overdue': return '#ef4444';
case 'draft': return '#6b7280';
case 'cancelled': return '#9ca3af';
default: return '#6b7280';
}
};
if (loading) {
return (
<div className={`ll-invoice-grid ll-invoice-grid-loading ${className}`}>
<div className="ll-invoice-grid-spinner" />
</div>
);
}
return (
<div className={`ll-invoice-grid ${className}`}>
{invoices.map((invoice) => (
<div
key={invoice.id}
className="ll-invoice-card"
onClick={() => onInvoiceClick?.(invoice)}
>
<div className="ll-invoice-card-header">
<span className="ll-invoice-card-number">#{invoice.invoiceNumber}</span>
<span
className="ll-invoice-card-status"
style={{ backgroundColor: getStatusColor(invoice.status) }}
>
{invoice.status}
</span>
</div>
<div className="ll-invoice-card-client">
<h4>{invoice.to.name}</h4>
{invoice.to.email && <p>{invoice.to.email}</p>}
</div>
<div className="ll-invoice-card-details">
<div className="ll-invoice-card-date">
<span>Issue Date</span>
<span>{formatDate(invoice.issueDate)}</span>
</div>
<div className="ll-invoice-card-date">
<span>Due Date</span>
<span>{formatDate(invoice.dueDate)}</span>
</div>
</div>
<div className="ll-invoice-card-amount">
<span>Total</span>
<span>{formatCurrency(invoice.total)}</span>
</div>
</div>
))}
{invoices.length === 0 && (
<div className="ll-invoice-grid-empty">
<p>No invoices found</p>
</div>
)}
</div>
);
};
// Invoice Form
export interface InvoiceFormProps {
/** Initial invoice data */
initialData?: Partial<InvoiceData>;
/** Save handler */
onSave?: (invoice: InvoiceData) => void;
/** Cancel handler */
onCancel?: () => void;
/** Clients for autocomplete */
clients?: { id: string; name: string; email?: string; address?: string }[];
/** Additional CSS classes */
className?: string;
}
export const InvoiceForm: React.FC<InvoiceFormProps> = ({
initialData,
onSave,
onCancel,
clients = [],
className = '',
}) => {
const [invoice, setInvoice] = useState<Partial<InvoiceData>>({
invoiceNumber: '',
status: 'draft',
issueDate: new Date(),
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
from: { name: '' },
to: { name: '' },
items: [],
subtotal: 0,
total: 0,
currency: '$',
...initialData,
});
const handleChange = (field: string, value: unknown) => {
setInvoice((prev) => ({ ...prev, [field]: value }));
};
const handleFromChange = (field: string, value: string) => {
setInvoice((prev) => ({
...prev,
from: { ...prev.from!, [field]: value },
}));
};
const handleToChange = (field: string, value: string) => {
setInvoice((prev) => ({
...prev,
to: { ...prev.to!, [field]: value },
}));
};
const addItem = () => {
const newItem: InvoiceItem = {
id: Date.now().toString(),
description: '',
quantity: 1,
unitPrice: 0,
total: 0,
};
setInvoice((prev) => ({
...prev,
items: [...(prev.items || []), newItem],
}));
};
const updateItem = (id: string, field: keyof InvoiceItem, value: unknown) => {
setInvoice((prev) => {
const items = prev.items?.map((item) => {
if (item.id !== id) return item;
const updated = { ...item, [field]: value };
if (field === 'quantity' || field === 'unitPrice') {
updated.total = updated.quantity * updated.unitPrice;
}
return updated;
}) || [];
const subtotal = items.reduce((sum, item) => sum + item.total, 0);
const tax = prev.taxRate ? subtotal * (prev.taxRate / 100) : prev.tax || 0;
let discount = 0;
if (prev.discount) {
discount = prev.discountType === 'percentage'
? subtotal * (prev.discount / 100)
: prev.discount;
}
const total = subtotal + tax - discount;
return { ...prev, items, subtotal, tax, total };
});
};
const removeItem = (id: string) => {
setInvoice((prev) => {
const items = prev.items?.filter((item) => item.id !== id) || [];
const subtotal = items.reduce((sum, item) => sum + item.total, 0);
const tax = prev.taxRate ? subtotal * (prev.taxRate / 100) : prev.tax || 0;
let discount = 0;
if (prev.discount) {
discount = prev.discountType === 'percentage'
? subtotal * (prev.discount / 100)
: prev.discount;
}
const total = subtotal + tax - discount;
return { ...prev, items, subtotal, tax, total };
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (invoice.invoiceNumber && invoice.from?.name && invoice.to?.name) {
onSave?.(invoice as InvoiceData);
}
};
return (
<form className={`ll-invoice-form ${className}`} onSubmit={handleSubmit}>
<div className="ll-invoice-form-header">
<h2>{initialData?.id ? 'Edit Invoice' : 'New Invoice'}</h2>
</div>
<div className="ll-invoice-form-section">
<h3>Invoice Details</h3>
<div className="ll-invoice-form-row">
<div className="ll-invoice-form-field">
<label>Invoice Number</label>
<input
type="text"
value={invoice.invoiceNumber || ''}
onChange={(e) => handleChange('invoiceNumber', e.target.value)}
required
/>
</div>
<div className="ll-invoice-form-field">
<label>Status</label>
<select
value={invoice.status}
onChange={(e) => handleChange('status', e.target.value)}
>
<option value="draft">Draft</option>
<option value="pending">Pending</option>
<option value="paid">Paid</option>
<option value="overdue">Overdue</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
<div className="ll-invoice-form-row">
<div className="ll-invoice-form-field">
<label>Issue Date</label>
<input
type="date"
value={invoice.issueDate?.toISOString().split('T')[0] || ''}
onChange={(e) => handleChange('issueDate', new Date(e.target.value))}
required
/>
</div>
<div className="ll-invoice-form-field">
<label>Due Date</label>
<input
type="date"
value={invoice.dueDate?.toISOString().split('T')[0] || ''}
onChange={(e) => handleChange('dueDate', new Date(e.target.value))}
required
/>
</div>
</div>
</div>
<div className="ll-invoice-form-section">
<h3>From</h3>
<div className="ll-invoice-form-field">
<label>Company Name</label>
<input
type="text"
value={invoice.from?.name || ''}
onChange={(e) => handleFromChange('name', e.target.value)}
required
/>
</div>
<div className="ll-invoice-form-field">
<label>Address</label>
<textarea
value={invoice.from?.address || ''}
onChange={(e) => handleFromChange('address', e.target.value)}
rows={2}
/>
</div>
<div className="ll-invoice-form-row">
<div className="ll-invoice-form-field">
<label>Email</label>
<input
type="email"
value={invoice.from?.email || ''}
onChange={(e) => handleFromChange('email', e.target.value)}
/>
</div>
<div className="ll-invoice-form-field">
<label>Phone</label>
<input
type="tel"
value={invoice.from?.phone || ''}
onChange={(e) => handleFromChange('phone', e.target.value)}
/>
</div>
</div>
</div>
<div className="ll-invoice-form-section">
<h3>Bill To</h3>
{clients.length > 0 && (
<div className="ll-invoice-form-field">
<label>Select Client</label>
<select
onChange={(e) => {
const client = clients.find((c) => c.id === e.target.value);
if (client) {
setInvoice((prev) => ({
...prev,
to: {
name: client.name,
email: client.email,
address: client.address,
},
}));
}
}}
>
<option value="">Select a client...</option>
{clients.map((client) => (
<option key={client.id} value={client.id}>{client.name}</option>
))}
</select>
</div>
)}
<div className="ll-invoice-form-field">
<label>Client Name</label>
<input
type="text"
value={invoice.to?.name || ''}
onChange={(e) => handleToChange('name', e.target.value)}
required
/>
</div>
<div className="ll-invoice-form-field">
<label>Address</label>
<textarea
value={invoice.to?.address || ''}
onChange={(e) => handleToChange('address', e.target.value)}
rows={2}
/>
</div>
<div className="ll-invoice-form-row">
<div className="ll-invoice-form-field">
<label>Email</label>
<input
type="email"
value={invoice.to?.email || ''}
onChange={(e) => handleToChange('email', e.target.value)}
/>
</div>
<div className="ll-invoice-form-field">
<label>Phone</label>
<input
type="tel"
value={invoice.to?.phone || ''}
onChange={(e) => handleToChange('phone', e.target.value)}
/>
</div>
</div>
</div>
<div className="ll-invoice-form-section">
<h3>Items</h3>
<table className="ll-invoice-form-items">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Total</th>
<th></th>
</tr>
</thead>
<tbody>
{invoice.items?.map((item) => (
<tr key={item.id}>
<td>
<input
type="text"
value={item.description}
onChange={(e) => updateItem(item.id, 'description', e.target.value)}
placeholder="Item description"
/>
</td>
<td>
<input
type="number"
min="1"
value={item.quantity}
onChange={(e) => updateItem(item.id, 'quantity', parseInt(e.target.value) || 1)}
/>
</td>
<td>
<input
type="number"
min="0"
step="0.01"
value={item.unitPrice}
onChange={(e) => updateItem(item.id, 'unitPrice', parseFloat(e.target.value) || 0)}
/>
</td>
<td className="ll-invoice-form-item-total">
{invoice.currency}{item.total.toFixed(2)}
</td>
<td>
<button
type="button"
className="ll-invoice-form-remove-item"
onClick={() => removeItem(item.id)}
>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
<button type="button" className="ll-invoice-form-add-item" onClick={addItem}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
Add Item
</button>
</div>
<div className="ll-invoice-form-section">
<h3>Additional</h3>
<div className="ll-invoice-form-field">
<label>Notes</label>
<textarea
value={invoice.notes || ''}
onChange={(e) => handleChange('notes', e.target.value)}
placeholder="Additional notes..."
rows={3}
/>
</div>
<div className="ll-invoice-form-field">
<label>Terms & Conditions</label>
<textarea
value={invoice.terms || ''}
onChange={(e) => handleChange('terms', e.target.value)}
placeholder="Payment terms..."
rows={3}
/>
</div>
</div>
<div className="ll-invoice-form-totals">
<div className="ll-invoice-form-total-row">
<span>Subtotal</span>
<span>{invoice.currency}{(invoice.subtotal || 0).toFixed(2)}</span>
</div>
<div className="ll-invoice-form-total-row ll-invoice-form-grand-total">
<span>Total</span>
<span>{invoice.currency}{(invoice.total || 0).toFixed(2)}</span>
</div>
</div>
<div className="ll-invoice-form-actions">
{onCancel && (
<button type="button" className="ll-invoice-form-btn-secondary" onClick={onCancel}>
Cancel
</button>
)}
<button type="submit" className="ll-invoice-form-btn-primary">
{initialData?.id ? 'Update Invoice' : 'Create Invoice'}
</button>
</div>
</form>
);
};
// Invoice Stats
export interface InvoiceStatsProps {
/** Total invoices */
total: number;
/** Paid amount */
paid: number;
/** Pending amount */
pending: number;
/** Overdue amount */
overdue: number;
/** Currency symbol */
currency?: string;
/** Additional CSS classes */
className?: string;
}
export const InvoiceStats: React.FC<InvoiceStatsProps> = ({
total,
paid,
pending,
overdue,
currency = '$',
className = '',
}) => {
const formatCurrency = (amount: number) => {
return `${currency}${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
};
return (
<div className={`ll-invoice-stats ${className}`}>
<div className="ll-invoice-stat">
<div className="ll-invoice-stat-icon ll-invoice-stat-total">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" />
</svg>
</div>
<div className="ll-invoice-stat-content">
<span className="ll-invoice-stat-value">{formatCurrency(total)}</span>
<span className="ll-invoice-stat-label">Total</span>
</div>
</div>
<div className="ll-invoice-stat">
<div className="ll-invoice-stat-icon ll-invoice-stat-paid">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
</div>
<div className="ll-invoice-stat-content">
<span className="ll-invoice-stat-value">{formatCurrency(paid)}</span>
<span className="ll-invoice-stat-label">Paid</span>
</div>
</div>
<div className="ll-invoice-stat">
<div className="ll-invoice-stat-icon ll-invoice-stat-pending">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z" />
</svg>
</div>
<div className="ll-invoice-stat-content">
<span className="ll-invoice-stat-value">{formatCurrency(pending)}</span>
<span className="ll-invoice-stat-label">Pending</span>
</div>
</div>
<div className="ll-invoice-stat">
<div className="ll-invoice-stat-icon ll-invoice-stat-overdue">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
</div>
<div className="ll-invoice-stat-content">
<span className="ll-invoice-stat-value">{formatCurrency(overdue)}</span>
<span className="ll-invoice-stat-label">Overdue</span>
</div>
</div>
</div>
);
};