EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
InvoiceFormNew.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useNavigate, useParams } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import '../styles/forms.css';
5import { apiFetch } from '../lib/api';
6import { notifySuccess, notifyError, notifyWarning } from '../utils/notifications';
7import { isAdmin } from '../utils/auth';
8
9const GST_RATE = 0.10; // 10% GST
10
11export default function InvoiceForm() {
12 const navigate = useNavigate();
13 const { id } = useParams();
14
15 // Invoice details
16 const [customerId, setCustomerId] = useState('');
17 const [customerSearch, setCustomerSearch] = useState('');
18 const [customers, setCustomers] = useState([]);
19 const [currency, setCurrency] = useState('AUD');
20 const [issuedDate, setIssuedDate] = useState(new Date().toISOString().split('T')[0]);
21 const [dueDate, setDueDate] = useState('');
22 const [description, setDescription] = useState('');
23 const [status, setStatus] = useState('draft');
24 const [paymentStatus, setPaymentStatus] = useState('unpaid');
25 const [purchaseOrderNumber, setPurchaseOrderNumber] = useState('');
26 const [internalNotes, setInternalNotes] = useState('');
27
28 // Line items
29 const [lineItems, setLineItems] = useState([]);
30 const [products, setProducts] = useState([]);
31
32 const [loading, setLoading] = useState(false);
33 const [loadingProducts, setLoadingProducts] = useState(true);
34
35 useEffect(() => { fetchProducts(); }, []);
36 useEffect(() => { if (id) fetchInvoice(); }, [id]);
37
38 const fetchInvoice = async () => {
39 try {
40 const token = localStorage.getItem('token');
41 const res = await apiFetch(`/invoices/${id}`, {
42 headers: { Authorization: `Bearer ${token}` }
43 });
44 if (!res.ok) throw new Error('Failed to load invoice');
45 const data = await res.json();
46
47 setCustomerId(data.customer_id || '');
48 setCustomerSearch(data.customer_name || '');
49 setCurrency(data.currency || 'AUD');
50 setIssuedDate(data.issued_date?.split('T')[0] || '');
51 setDueDate(data.due_date?.split('T')[0] || '');
52 setDescription(data.description || '');
53 setStatus(data.status || 'draft');
54 setPaymentStatus(data.payment_status || 'unpaid');
55 setPurchaseOrderNumber(data.purchase_order_number || '');
56 setInternalNotes(data.internal_notes || '');
57
58 // Load line items
59 setLineItems((data.line_items || []).map(item => ({
60 line_item_id: item.line_item_id,
61 product_id: item.product_id,
62 description: item.description || '',
63 quantity: Number(item.quantity) || 1,
64 unit_price_ex: Number(item.unit_price_ex) || 0,
65 unit_price_inc: Number(item.unit_price_inc) || 0,
66 gst_amount: Number(item.gst_amount) || 0,
67 productSearch: '',
68 showProducts: false,
69 searchResults: []
70 })));
71 } catch (err) {
72 console.error(err);
73 alert('Failed to load invoice: ' + err.message);
74 }
75 };
76
77 const fetchProducts = async () => {
78 setLoadingProducts(true);
79 try {
80 const token = localStorage.getItem('token');
81 const res = await apiFetch('/products?limit=1000', {
82 headers: { Authorization: `Bearer ${token}` }
83 });
84 if (!res.ok) throw new Error('Failed to load products');
85 const data = await res.json();
86 setProducts(data.products || []);
87 } catch (err) {
88 console.error(err);
89 } finally {
90 setLoadingProducts(false);
91 }
92 };
93
94 const searchCustomers = async (query) => {
95 try {
96 const token = localStorage.getItem('token');
97 const res = await apiFetch(
98 `/customers?search=${encodeURIComponent(query)}&limit=10`,
99 { headers: { Authorization: `Bearer ${token}` } }
100 );
101 if (!res.ok) throw new Error('Failed to search customers');
102 const data = await res.json();
103 setCustomers(data.customers || []);
104 } catch (err) {
105 console.error(err);
106 }
107 };
108
109 useEffect(() => {
110 if (customerSearch && !customerId) {
111 const timer = setTimeout(() => searchCustomers(customerSearch), 300);
112 return () => clearTimeout(timer);
113 } else if (!customerSearch) {
114 setCustomers([]);
115 }
116 }, [customerSearch]);
117
118 const addLineItem = () => {
119 setLineItems(items => [...items, {
120 product_id: null,
121 description: '',
122 quantity: 1,
123 unit_price_ex: 0,
124 unit_price_inc: 0,
125 gst_amount: 0,
126 productSearch: '',
127 showProducts: false,
128 searchResults: []
129 }]);
130 };
131
132 const removeLineItem = (idx) => {
133 setLineItems(items => items.filter((_, i) => i !== idx));
134 };
135
136 const searchProducts = async (search, idx) => {
137 try {
138 const token = localStorage.getItem('token');
139 const res = await apiFetch(
140 `/products?search=${encodeURIComponent(search)}&limit=10`,
141 { headers: { Authorization: `Bearer ${token}` } }
142 );
143 if (!res.ok) throw new Error('Product search failed');
144 const data = await res.json();
145 setLineItems(items => items.map((it, i) =>
146 i === idx ? { ...it, searchResults: data.products || [] } : it
147 ));
148 } catch (err) {
149 console.error('Search failed:', err);
150 }
151 };
152
153 const calculateLinePrices = (quantity, unitPriceEx, gstFree = false) => {
154 const qty = Number(quantity) || 0;
155 const priceEx = Number(unitPriceEx) || 0;
156 const gstAmount = gstFree ? 0 : priceEx * GST_RATE;
157 const unitPriceInc = priceEx + gstAmount;
158 const lineTotalEx = qty * priceEx;
159 const lineGst = gstFree ? 0 : lineTotalEx * GST_RATE;
160 const lineTotalInc = lineTotalEx + lineGst;
161
162 return {
163 unit_price_inc: Number(unitPriceInc.toFixed(2)),
164 gst_amount: Number(gstAmount.toFixed(2)),
165 line_total_ex: Number(lineTotalEx.toFixed(2)),
166 line_gst: Number(lineGst.toFixed(2)),
167 line_total_inc: Number(lineTotalInc.toFixed(2))
168 };
169 };
170
171 const updateLineItem = (idx, patch) => {
172 setLineItems(items => items.map((it, i) => {
173 if (i !== idx) return it;
174
175 const updated = { ...it, ...patch };
176
177 // Recalculate prices if quantity or unit_price_ex changed
178 if (patch.quantity !== undefined || patch.unit_price_ex !== undefined) {
179 const prices = calculateLinePrices(
180 updated.quantity,
181 updated.unit_price_ex,
182 updated.gst_free
183 );
184 Object.assign(updated, prices);
185 }
186
187 return updated;
188 }));
189
190 // Trigger product search if needed
191 if (patch.productSearch !== undefined && patch.productSearch) {
192 setTimeout(() => searchProducts(patch.productSearch, idx), 300);
193 }
194 };
195
196 const calculateTotals = () => {
197 let subtotal = 0;
198 let gstTotal = 0;
199
200 lineItems.forEach(item => {
201 const qty = Number(item.quantity) || 0;
202 const priceEx = Number(item.unit_price_ex) || 0;
203 const lineTotalEx = qty * priceEx;
204 const lineGst = item.gst_free ? 0 : lineTotalEx * GST_RATE;
205
206 subtotal += lineTotalEx;
207 gstTotal += lineGst;
208 });
209
210 const total = subtotal + gstTotal;
211
212 return {
213 subtotal: subtotal.toFixed(2),
214 gstTotal: gstTotal.toFixed(2),
215 total: total.toFixed(2)
216 };
217 };
218
219 // Administrative functions (admin-only)
220 const markAsUnpaid = async () => {
221 if (!confirm(`Mark invoice #${id} as unpaid? This will allow you to void or delete it. Use only for test invoices.`)) return;
222 try {
223 const token = localStorage.getItem('token');
224 const res = await apiFetch(`/invoices/${id}`, {
225 method: 'PUT',
226 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
227 body: JSON.stringify({
228 customer_id: customerId,
229 currency,
230 issued_date: issuedDate,
231 due_date: dueDate,
232 description,
233 status,
234 payment_status: 'pending', // Force to pending
235 purchase_order_number: purchaseOrderNumber,
236 internal_notes: internalNotes,
237 line_items: lineItems
238 })
239 });
240 if (!res.ok) throw new Error(await res.text() || 'Failed to update invoice');
241 setPaymentStatus('pending');
242 notifySuccess(`Invoice #${id} marked as unpaid. You can now void or delete it.`);
243 } catch (err) {
244 console.error('[Mark Unpaid] Error:', err);
245 notifyError(err.message || 'Failed to mark invoice as unpaid');
246 }
247 };
248
249 const voidInvoice = async () => {
250 if (!confirm(`Void invoice #${id}? This will restock items and mark it as void.`)) return;
251 const reason = window.prompt('Enter a reason for voiding this invoice (optional):', '') || '';
252 try {
253 const token = localStorage.getItem('token');
254 const res = await apiFetch(`/invoices/${id}/void`, {
255 method: 'POST',
256 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
257 body: JSON.stringify({ reason })
258 });
259 if (!res.ok) {
260 const txt = await res.text();
261 throw new Error(txt || 'Failed to void invoice');
262 }
263 notifySuccess('Invoice voided successfully');
264 navigate('/invoices');
265 } catch (err) {
266 notifyError(err.message || 'Failed to void invoice');
267 }
268 };
269
270 const deleteInvoice = async () => {
271 console.log('[Delete] Attempting to delete invoice');
272 const isDraftOrVoid = status === 'draft' || status === 'void';
273 const isPaid = paymentStatus === 'paid';
274
275 if (!isDraftOrVoid) {
276 notifyWarning(`Cannot delete invoice with status: ${status}. Only draft or void invoices can be deleted.`);
277 return;
278 }
279 if (isPaid) {
280 notifyError('Cannot delete a paid invoice. Use "Mark as Unpaid" first.');
281 return;
282 }
283 if (!confirm(`Delete invoice #${id}? This will permanently remove it and restock any products.`)) {
284 console.log('[Delete] User cancelled deletion');
285 return;
286 }
287
288 try {
289 const token = localStorage.getItem('token');
290 const res = await apiFetch(`/invoices/${id}`, {
291 method: 'DELETE',
292 headers: { Authorization: `Bearer ${token}` }
293 });
294 if (!res.ok) {
295 const text = await res.text();
296 throw new Error(text || 'Failed to delete invoice');
297 }
298 console.log('[Delete] Invoice deleted successfully');
299 notifySuccess(`Invoice #${id} deleted successfully`);
300 navigate('/invoices');
301 } catch (err) {
302 console.error('[Delete] Error:', err);
303 notifyError(err.message || 'Failed to delete invoice');
304 }
305 };
306
307 const handleSave = async () => {
308 if (!customerId) {
309 await notifyWarning('Customer Required', 'Please select a customer before saving');
310 return;
311 }
312
313 setLoading(true);
314 try {
315 const token = localStorage.getItem('token');
316 const totals = calculateTotals();
317
318 const payload = {
319 customer_id: customerId,
320 currency,
321 issued_date: issuedDate || null,
322 due_date: dueDate || null,
323 description,
324 status,
325 payment_status: paymentStatus,
326 purchase_order_number: purchaseOrderNumber || null,
327 internal_notes: internalNotes || null,
328 subtotal: Number(totals.subtotal),
329 tax_total: Number(totals.gstTotal),
330 total: Number(totals.total),
331 line_items: lineItems.map(item => ({
332 line_item_id: item.line_item_id,
333 product_id: item.product_id,
334 description: item.description,
335 quantity: Number(item.quantity),
336 unit_price_ex: Number(item.unit_price_ex),
337 unit_price_inc: Number(item.unit_price_inc),
338 gst_amount: Number(item.gst_amount)
339 }))
340 };
341
342 const url = id ? `/invoices/${id}` : '/invoices';
343 const method = id ? 'PUT' : 'POST';
344
345 const res = await apiFetch(url, {
346 method,
347 headers: {
348 'Content-Type': 'application/json',
349 Authorization: `Bearer ${token}`
350 },
351 body: JSON.stringify(payload)
352 });
353
354 if (!res.ok) {
355 const txt = await res.text();
356 const errorMsg = txt || 'Save failed';
357 await notifyError(id ? 'Update Failed' : 'Create Failed', errorMsg);
358 throw new Error(errorMsg);
359 }
360
361 const result = await res.json();
362 const invoice = result.invoice || result;
363
364 await notifySuccess(id ? 'Invoice Updated' : 'Invoice Created', `Invoice #${invoice.invoice_id} has been ${id ? 'updated' : 'created'} successfully`);
365 navigate(`/invoices/${invoice.invoice_id}`);
366 } catch (err) {
367 // Error already notified above
368 } finally {
369 setLoading(false);
370 }
371 };
372
373 const totals = calculateTotals();
374
375 return (
376 <MainLayout>
377 <div className="page-content">
378 <div className="page-header">
379 <h2>{id ? 'Edit Invoice' : 'Create Invoice'}</h2>
380 </div>
381
382 <div className="form-container">
383 <div className="form-grid">
384 {/* Basic Information */}
385 <div className="form-section">
386 <h3>Invoice Details</h3>
387
388 <div className="form-row">
389 <div className="form-field">
390 <label>Customer<span className="required">*</span></label>
391 <div style={{ position: 'relative' }}>
392 <input
393 type="text"
394 value={customerSearch}
395 onChange={e => {
396 setCustomerSearch(e.target.value);
397 setCustomerId('');
398 }}
399 placeholder="Search customers..."
400 required
401 />
402 {customers.length > 0 && (
403 <div style={{
404 position: 'absolute',
405 top: '100%',
406 left: 0,
407 right: 0,
408 background: 'var(--surface)',
409 border: '1px solid var(--border)',
410 borderRadius: '4px',
411 zIndex: 1000,
412 maxHeight: '200px',
413 overflowY: 'auto',
414 boxShadow: '0 4px 8px rgba(0,0,0,0.1)'
415 }}>
416 {customers.map(c => (
417 <div
418 key={c.customer_id}
419 style={{
420 padding: '12px',
421 cursor: 'pointer',
422 borderBottom: '1px solid var(--border)'
423 }}
424 onClick={() => {
425 setCustomerId(c.customer_id);
426 setCustomerSearch(c.name);
427 setCustomers([]);
428 }}
429 >
430 <strong>{c.name}</strong>
431 {c.email && <div style={{ fontSize: '0.9em', color: 'var(--text-muted)' }}>{c.email}</div>}
432 </div>
433 ))}
434 </div>
435 )}
436 </div>
437 </div>
438
439 <div className="form-field">
440 <label>Purchase Order Number</label>
441 <input
442 type="text"
443 value={purchaseOrderNumber}
444 onChange={e => setPurchaseOrderNumber(e.target.value)}
445 placeholder="PO-12345"
446 />
447 </div>
448 </div>
449
450 <div className="form-row">
451 <div className="form-field">
452 <label>Issued Date</label>
453 <input
454 type="date"
455 value={issuedDate}
456 onChange={e => setIssuedDate(e.target.value)}
457 />
458 </div>
459
460 <div className="form-field">
461 <label>Due Date</label>
462 <input
463 type="date"
464 value={dueDate}
465 onChange={e => setDueDate(e.target.value)}
466 />
467 </div>
468
469 <div className="form-field">
470 <label>Currency</label>
471 <select value={currency} onChange={e => setCurrency(e.target.value)}>
472 <option value="AUD">AUD</option>
473 <option value="USD">USD</option>
474 <option value="EUR">EUR</option>
475 <option value="GBP">GBP</option>
476 </select>
477 </div>
478 </div>
479
480 <div className="form-row">
481 <div className="form-field">
482 <label>Status</label>
483 <select value={status} onChange={e => setStatus(e.target.value)}>
484 <option value="draft">Draft</option>
485 <option value="sent">Sent</option>
486 <option value="approved">Approved</option>
487 <option value="cancelled">Cancelled</option>
488 </select>
489 </div>
490
491 <div className="form-field">
492 <label>Payment Status</label>
493 <select value={paymentStatus} onChange={e => setPaymentStatus(e.target.value)}>
494 <option value="unpaid">Unpaid</option>
495 <option value="partial">Partially Paid</option>
496 <option value="paid">Paid</option>
497 <option value="overdue">Overdue</option>
498 </select>
499 </div>
500 </div>
501
502 <div className="form-field">
503 <label>Description</label>
504 <textarea
505 value={description}
506 onChange={e => setDescription(e.target.value)}
507 rows={2}
508 placeholder="Brief description of the invoice..."
509 />
510 </div>
511
512 <div className="form-field">
513 <label>Internal Notes</label>
514 <textarea
515 value={internalNotes}
516 onChange={e => setInternalNotes(e.target.value)}
517 rows={3}
518 placeholder="Notes for internal use only (not visible to customer)..."
519 />
520 <span className="field-hint">These notes are for internal use and won't appear on the invoice</span>
521 </div>
522 </div>
523
524 {/* Line Items */}
525 <div className="form-section">
526 <h3>Line Items</h3>
527
528 {loadingProducts ? (
529 <div>Loading products...</div>
530 ) : (
531 <>
532 <div style={{
533 display: 'grid',
534 gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr auto',
535 gap: '12px',
536 marginBottom: '12px',
537 fontWeight: 'bold',
538 fontSize: '0.9em',
539 color: 'var(--text-muted)'
540 }}>
541 <div>Product/Description</div>
542 <div>Qty</div>
543 <div>Unit Price Ex</div>
544 <div>Unit Price Inc</div>
545 <div>GST</div>
546 <div></div>
547 </div>
548
549 {lineItems.map((item, idx) => {
550 const linePrices = calculateLinePrices(item.quantity, item.unit_price_ex, item.gst_free);
551
552 return (
553 <div key={idx} style={{
554 marginBottom: '16px',
555 padding: '12px',
556 background: 'var(--bg-secondary)',
557 borderRadius: '6px',
558 border: '1px solid var(--border)'
559 }}>
560 <div style={{
561 display: 'grid',
562 gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr auto',
563 gap: '12px',
564 alignItems: 'start'
565 }}>
566 <div style={{ position: 'relative' }}>
567 <input
568 type="text"
569 value={item.productSearch || products.find(p => p.product_id === item.product_id)?.name || ''}
570 onChange={e => {
571 const search = e.target.value;
572 updateLineItem(idx, { productSearch: search, product_id: null });
573 }}
574 onFocus={() => updateLineItem(idx, { showProducts: true })}
575 onBlur={() => setTimeout(() => updateLineItem(idx, { showProducts: false }), 200)}
576 placeholder="Search products..."
577 style={{ marginBottom: '8px' }}
578 />
579
580 {item.showProducts && item.searchResults?.length > 0 && (
581 <div style={{
582 position: 'absolute',
583 top: '100%',
584 left: 0,
585 right: 0,
586 background: 'var(--surface)',
587 border: '1px solid var(--border)',
588 borderRadius: '4px',
589 zIndex: 1000,
590 maxHeight: '200px',
591 overflowY: 'auto',
592 boxShadow: '0 4px 8px rgba(0,0,0,0.1)'
593 }}>
594 {item.searchResults.map(p => (
595 <div
596 key={p.product_id}
597 style={{
598 padding: '8px',
599 cursor: 'pointer',
600 borderBottom: '1px solid var(--border)'
601 }}
602 onMouseDown={e => {
603 e.preventDefault();
604 updateLineItem(idx, {
605 product_id: p.product_id,
606 description: p.name,
607 unit_price_ex: Number(p.price_ex_tax || 0),
608 gst_free: p.gst_free || false,
609 productSearch: p.name,
610 showProducts: false
611 });
612 }}
613 >
614 <strong>{p.name}</strong>
615 {p.sku && <div style={{ fontSize: '0.85em', color: 'var(--text-muted)' }}>SKU: {p.sku}</div>}
616 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)' }}>
617 ${Number(p.price_ex_tax || 0).toFixed(2)} ex GST
618 </div>
619 </div>
620 ))}
621 </div>
622 )}
623
624 <textarea
625 value={item.description}
626 onChange={e => updateLineItem(idx, { description: e.target.value })}
627 placeholder="Description..."
628 rows={2}
629 />
630 </div>
631
632 <input
633 type="number"
634 value={item.quantity}
635 onChange={e => updateLineItem(idx, { quantity: Number(e.target.value) })}
636 step="0.01"
637 min="0"
638 />
639
640 <input
641 type="number"
642 value={item.unit_price_ex}
643 onChange={e => updateLineItem(idx, { unit_price_ex: Number(e.target.value) })}
644 step="0.01"
645 min="0"
646 />
647
648 <div style={{ padding: '10px 0', color: 'var(--text-muted)' }}>
649 ${linePrices.unit_price_inc.toFixed(2)}
650 </div>
651
652 <div style={{ padding: '10px 0', color: 'var(--text-muted)' }}>
653 ${linePrices.gst_amount.toFixed(2)}
654 </div>
655
656 <button
657 type="button"
658 className="btn"
659 onClick={() => removeLineItem(idx)}
660 style={{ padding: '8px', minHeight: 'auto' }}
661 >
662 <span className="material-symbols-outlined">delete</span>
663 </button>
664 </div>
665
666 <div style={{
667 marginTop: '8px',
668 paddingTop: '8px',
669 borderTop: '1px solid var(--border)',
670 fontSize: '0.9em',
671 color: 'var(--text-muted)'
672 }}>
673 Line Total: ${linePrices.line_total_ex.toFixed(2)} + ${linePrices.line_gst.toFixed(2)} GST =
674 <strong style={{ marginLeft: '4px', color: 'var(--text)' }}>
675 ${linePrices.line_total_inc.toFixed(2)}
676 </strong>
677 </div>
678 </div>
679 );
680 })}
681
682 <button
683 type="button"
684 className="btn"
685 onClick={addLineItem}
686 style={{ marginTop: '8px' }}
687 >
688 <span className="material-symbols-outlined">add</span>
689 Add Line Item
690 </button>
691 </>
692 )}
693 </div>
694
695 {/* Totals Summary */}
696 <div className="form-section">
697 <h3>Summary</h3>
698 <div style={{
699 display: 'flex',
700 flexDirection: 'column',
701 gap: '12px',
702 fontSize: '1.1em'
703 }}>
704 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
705 <span>Subtotal (Ex GST):</span>
706 <strong>${totals.subtotal}</strong>
707 </div>
708 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
709 <span>GST Total:</span>
710 <strong>${totals.gstTotal}</strong>
711 </div>
712 <div style={{
713 display: 'flex',
714 justifyContent: 'space-between',
715 paddingTop: '12px',
716 borderTop: '2px solid var(--border)',
717 fontSize: '1.2em',
718 color: 'var(--primary)'
719 }}>
720 <span>Total (Inc GST):</span>
721 <strong>{currency} ${totals.total}</strong>
722 </div>
723 </div>
724 </div>
725 </div>
726
727 <div className="form-actions">
728 <button
729 type="button"
730 className="btn"
731 onClick={handleSave}
732 disabled={loading}
733 >
734 {loading ? 'Saving...' : (
735 <>
736 <span className="material-symbols-outlined">save</span>
737 {id ? 'Update Invoice' : 'Create Invoice'}
738 </>
739 )}
740 </button>
741 <button
742 type="button"
743 className="btn"
744 onClick={() => navigate('/invoices')}
745 >
746 <span className="material-symbols-outlined">close</span>
747 Cancel
748 </button>
749
750 {/* Admin actions - only show when editing existing invoice */}
751 {id && isAdmin() && (
752 <div style={{ marginLeft: 'auto', display: 'flex', gap: '10px' }}>
753 {/* Mark as Unpaid - only for paid invoices */}
754 {paymentStatus === 'paid' && (
755 <button
756 type="button"
757 className="btn-icon"
758 onClick={markAsUnpaid}
759 title="Mark as Unpaid (for test cleanup)"
760 style={{ color: '#f59e0b', fontSize: '24px', padding: '8px' }}
761 >
762 <span className="material-symbols-outlined">money_off</span>
763 </button>
764 )}
765
766 {/* Void button */}
767 <button
768 type="button"
769 className="btn-icon"
770 onClick={voidInvoice}
771 disabled={status === 'void' || paymentStatus === 'paid'}
772 title={
773 status === 'void'
774 ? 'Already voided'
775 : paymentStatus === 'paid'
776 ? 'Cannot void paid invoices (use Mark as Unpaid first)'
777 : 'Void invoice'
778 }
779 style={{
780 opacity: (status === 'void' || paymentStatus === 'paid') ? 0.3 : 1,
781 cursor: (status === 'void' || paymentStatus === 'paid') ? 'not-allowed' : 'pointer',
782 fontSize: '24px',
783 padding: '8px'
784 }}
785 >
786 <span className="material-symbols-outlined">block</span>
787 </button>
788
789 {/* Delete button */}
790 <button
791 type="button"
792 className="btn-icon"
793 onClick={deleteInvoice}
794 disabled={!(status === 'draft' || status === 'void') || paymentStatus === 'paid'}
795 title={
796 paymentStatus === 'paid'
797 ? 'Cannot delete paid invoices'
798 : !(status === 'draft' || status === 'void')
799 ? 'Cannot delete issued invoices (only draft or void)'
800 : 'Delete invoice (will restock products)'
801 }
802 style={{
803 opacity: (!(status === 'draft' || status === 'void') || paymentStatus === 'paid') ? 0.3 : 1,
804 cursor: (!(status === 'draft' || status === 'void') || paymentStatus === 'paid') ? 'not-allowed' : 'pointer',
805 fontSize: '24px',
806 padding: '8px'
807 }}
808 >
809 <span className="material-symbols-outlined">delete</span>
810 </button>
811 </div>
812 )}
813 </div>
814 </div>
815 </div>
816 </MainLayout>
817 );
818}