1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4// import removed: TenantSelector
5import '../styles/theme.css';
6import './Invoices.css';
7import { apiFetch } from '../lib/api';
8import { isAdmin } from '../utils/auth';
9import PayInvoiceModal from '../components/PayInvoiceModal';
12 const [invoices, setInvoices] = useState([]);
13 const [loading, setLoading] = useState(true);
14 const [error, setError] = useState(null);
15 const [page, setPage] = useState(1);
16 const [totalPages, setTotalPages] = useState(1);
17 const [search, setSearch] = useState('');
18 const [dateFrom, setDateFrom] = useState('');
19 const [dateTo, setDateTo] = useState('');
20 const [paymentStatus, setPaymentStatus] = useState('all');
21 const [showFilters, setShowFilters] = useState(false);
22 const [isRootTenant, setIsRootTenant] = useState(false);
23 const [tenantMap, setTenantMap] = useState({});
24 const [showPayModal, setShowPayModal] = useState(false);
25 const [selectedInvoice, setSelectedInvoice] = useState(null);
26 // Remove selectedTenantId and use tenantId from JWT only
27 const token = localStorage.getItem('token');
31 const payload = token.split('.')[1];
32 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
33 tenantId = decoded.tenant_id || decoded.tenantId || '';
37 // Reload invoices when tenantId changes
41 const navigate = useNavigate();
43 const refetch = async () => {
45 const token = localStorage.getItem('token');
46 let url = '/invoices';
47 const params = new URLSearchParams();
48 params.set('page', page);
49 params.set('limit', 10);
50 if (search) params.set('search', search);
51 if (dateFrom) params.set('date_from', dateFrom);
52 if (dateTo) params.set('date_to', dateTo);
53 if (paymentStatus && paymentStatus !== 'all') params.set('payment_status', paymentStatus);
54 const queryString = params.toString();
55 if (queryString) url += `?${queryString}`;
56 const response = await apiFetch(url, { headers: { Authorization: `Bearer ${token}` } });
57 if (!response.ok) throw new Error('Failed to load invoices');
58 const data = await response.json();
59 setInvoices(data.invoices);
60 setTotalPages(Math.ceil(data.total / 10));
63 setError(err.message);
67 const voidInvoice = async (invoice) => {
68 if (!confirm(`Void invoice #${invoice.invoice_id}? This will restock items and mark it as void.`)) return;
69 const reason = window.prompt('Enter a reason for voiding this invoice (optional):', '') || '';
71 const token = localStorage.getItem('token');
72 const url = `/invoices/${invoice.invoice_id}/void`;
73 const res = await apiFetch(url, {
75 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
76 body: JSON.stringify({ reason })
79 const txt = await res.text();
80 throw new Error(txt || 'Failed to void invoice');
83 alert('Invoice voided');
85 alert(err.message || 'Failed to void invoice');
89 const deleteInvoice = async (invoice) => {
90 console.log('[Delete] Attempting to delete invoice:', invoice);
92 // Check if delete is allowed
93 const isDraftOrVoid = invoice.status === 'draft' || invoice.status === 'void';
94 const isPaid = invoice.payment_status === 'paid';
97 alert(`Cannot delete invoice with status: ${invoice.status}. Only draft or void invoices can be deleted.`);
102 alert('Cannot delete a paid invoice.');
106 if (!confirm(`Delete invoice #${invoice.invoice_id}? This will permanently remove it and restock any products.`)) {
107 console.log('[Delete] User cancelled deletion');
111 console.log('[Delete] Confirmed - making DELETE request');
113 const token = localStorage.getItem('token');
114 const url = `/invoices/${invoice.invoice_id}`;
115 console.log('[Delete] DELETE request to:', url);
117 const res = await apiFetch(url, {
119 headers: { Authorization: `Bearer ${token}` }
122 console.log('[Delete] Response status:', res.status, res.ok);
125 const txt = await res.text();
126 console.error('[Delete] Error response:', txt);
127 throw new Error(txt || 'Failed to delete invoice');
130 // Remove locally to avoid extra fetch latency
131 setInvoices(list => list.filter(i => i.invoice_id !== invoice.invoice_id));
132 alert(`Invoice #${invoice.invoice_id} deleted successfully`);
133 console.log('[Delete] Success - invoice removed from list');
135 console.error('[Delete] Exception:', err);
136 alert(err.message || 'Failed to delete invoice');
140 const markAsUnpaid = async (invoice) => {
141 if (!confirm(`Mark invoice #${invoice.invoice_id} as unpaid? This will allow you to void or delete it. Use only for test invoices.`)) {
146 const token = localStorage.getItem('token');
147 const res = await apiFetch(`/invoices/${invoice.invoice_id}`, {
150 Authorization: `Bearer ${token}`,
151 'Content-Type': 'application/json'
153 body: JSON.stringify({
155 payment_status: 'pending'
160 const txt = await res.text();
161 throw new Error(txt || 'Failed to update invoice');
165 setInvoices(list => list.map(i =>
166 i.invoice_id === invoice.invoice_id
167 ? { ...i, payment_status: 'pending' }
171 alert(`Invoice #${invoice.invoice_id} marked as unpaid. You can now void or delete it.`);
173 console.error('[Mark Unpaid] Error:', err);
174 alert(err.message || 'Failed to mark invoice as unpaid');
178 // Detect if user is root tenant (MSP)
180 const detectRootTenant = async () => {
182 const token = localStorage.getItem('token');
183 const res = await apiFetch('/tenants', {
184 headers: { Authorization: `Bearer ${token}` }
187 setIsRootTenant(true);
188 const data = await res.json();
190 data.tenants?.forEach(t => {
191 map[t.tenant_id] = t.name;
196 // Silently fail - 403 is expected for non-MSP users
203 const fetchInvoices = async () => {
205 const token = localStorage.getItem('token');
206 let url = '/invoices';
207 const params = new URLSearchParams();
208 params.set('page', page);
209 params.set('limit', 10);
210 if (search) params.set('search', search);
211 if (dateFrom) params.set('date_from', dateFrom);
212 if (dateTo) params.set('date_to', dateTo);
213 if (paymentStatus && paymentStatus !== 'all') params.set('payment_status', paymentStatus);
214 const queryString = params.toString();
215 if (queryString) url += `?${queryString}`;
216 const response = await apiFetch(url, {
217 headers: { Authorization: `Bearer ${token}` }
219 if (!response.ok) throw new Error('Failed to load invoices');
220 const data = await response.json();
221 setInvoices(data.invoices);
222 setTotalPages(Math.ceil(data.total / 10));
225 setError(err.message);
232 }, [page, search, dateFrom, dateTo, paymentStatus]);
236 <div className="page-container">
237 <div className="page-header">
238 <div className="header-content">
240 <div className="header-actions">
241 <div className="search-box">
242 <span className="material-symbols-outlined">search</span>
245 placeholder="Search invoice#, customer..."
247 onChange={(e) => { setSearch(e.target.value); setPage(1); }}
250 <button className="btn btn-secondary" onClick={() => setShowFilters(!showFilters)}>
251 <span className="material-symbols-outlined">filter_list</span>
254 <button className="btn" onClick={() => navigate('/invoices/new')}>
255 <span className="material-symbols-outlined">add</span>
263 <div className="filters-panel">
264 <div className="filter-group">
265 <label>From Date:</label>
269 onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
272 <div className="filter-group">
273 <label>To Date:</label>
277 onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
280 <div className="filter-group">
281 <label>Payment Status:</label>
283 value={paymentStatus}
284 onChange={(e) => { setPaymentStatus(e.target.value); setPage(1); }}
286 <option value="all">All Statuses</option>
287 <option value="unpaid">Unpaid</option>
288 <option value="partial">Partially Paid</option>
289 <option value="paid">Paid</option>
290 <option value="overdue">Overdue</option>
293 <button className="btn btn-sm" onClick={() => {
297 setPaymentStatus('all');
300 <span className="material-symbols-outlined">clear</span>
306 {loading && <div>Loading...</div>}
307 {error && <div className="error">Error: {error}</div>}
309 {!loading && !error && (
310 <div className="table-container">
311 {invoices.length === 0 ? (
312 <div className="empty-state">
313 <span className="material-symbols-outlined">receipt_long</span>
314 <h3>No invoices found</h3>
315 <p>Try adjusting your search or filters, or create a new invoice.</p>
319 <table className="data-table">
323 {isRootTenant && <th>Tenant</th>}
334 {invoices.map(invoice => (
335 <tr key={invoice.invoice_id}>
336 <td className="invoice-number">{invoice.invoice_number || `#${invoice.invoice_id}`}</td>
337 {isRootTenant && <td>{invoice.tenant_id === 1 ? 'Root Tenant' : (invoice.tenant_name || tenantMap[invoice.tenant_id] || '-')}</td>}
338 <td>{invoice.customer_name || 'N/A'}</td>
339 <td>{invoice.issued_date ? new Date(invoice.issued_date).toLocaleDateString() : '-'}</td>
340 <td>{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '-'}</td>
341 <td className="amount">${parseFloat(invoice.total || invoice.amount || 0).toFixed(2)}</td>
342 <td><span className={`status-badge ${invoice.payment_status || 'pending'}`}>{invoice.payment_status || 'pending'}</span></td>
343 <td><span className={`status-badge ${invoice.status}`}>{invoice.status}</span></td>
344 <td className="actions"> {/* Pay Now button - only show for unpaid invoices */}
345 {invoice.payment_status !== 'paid' && invoice.status !== 'void' && (
349 setSelectedInvoice(invoice);
350 setShowPayModal(true);
353 style={{ color: '#10b981' }}
355 <span className="material-symbols-outlined">payments</span>
357 )} <button className="btn-icon" onClick={() => navigate(`/invoices/${invoice.invoice_id}`)} title="View">
358 <span className="material-symbols-outlined">visibility</span>
360 <button className="btn-icon" onClick={() => navigate(`/invoices/${invoice.invoice_id}/edit`)} title="Edit">
361 <span className="material-symbols-outlined">edit</span>
364 {/* Mark as Unpaid - Admin only, for test invoices */}
365 {isAdmin() && invoice.payment_status === 'paid' && (
368 title="Mark as Unpaid (for test cleanup)"
369 onClick={() => markAsUnpaid(invoice)}
370 style={{ color: '#f59e0b' }}
372 <span className="material-symbols-outlined">money_off</span>
376 {/* Void invoice (not if already void or paid) */}
380 invoice.payment_status === 'paid'
381 ? 'Cannot void paid invoices (use Mark as Unpaid first)'
382 : invoice.status === 'void'
386 onClick={() => voidInvoice(invoice)}
387 disabled={invoice.status === 'void' || invoice.payment_status === 'paid'}
389 opacity: (invoice.status === 'void' || invoice.payment_status === 'paid') ? 0.3 : 1,
390 cursor: (invoice.status === 'void' || invoice.payment_status === 'paid') ? 'not-allowed' : 'pointer'
393 <span className="material-symbols-outlined">block</span>
395 {/* Delete invoice (only draft or void and not paid) - Admin only */}
400 invoice.payment_status === 'paid'
401 ? 'Cannot delete paid invoices'
402 : (invoice.status !== 'draft' && invoice.status !== 'void')
403 ? `Cannot delete ${invoice.status} invoices (only draft or void)`
404 : 'Delete invoice (will restock products)'
406 onClick={() => deleteInvoice(invoice)}
407 disabled={!(invoice.status === 'draft' || invoice.status === 'void') || invoice.payment_status === 'paid'}
409 opacity: (!(invoice.status === 'draft' || invoice.status === 'void') || invoice.payment_status === 'paid') ? 0.3 : 1,
410 cursor: (!(invoice.status === 'draft' || invoice.status === 'void') || invoice.payment_status === 'paid') ? 'not-allowed' : 'pointer'
413 <span className="material-symbols-outlined">delete</span>
422 <div className="pagination">
425 onClick={() => setPage(p => Math.max(1, p - 1))}
426 disabled={page === 1}
428 <span className="material-symbols-outlined">navigate_before</span>
431 <span className="page-info">Page {page} of {totalPages}</span>
434 onClick={() => setPage(p => Math.min(totalPages, p + 1))}
435 disabled={page === totalPages}
438 <span className="material-symbols-outlined">navigate_next</span>
447 {/* Payment Modal */}
448 {showPayModal && selectedInvoice && (
450 invoice={selectedInvoice}
452 setShowPayModal(false);
453 setSelectedInvoice(null);
456 refetch(); // Reload invoices to show updated payment status
464export default Invoices;