1import { useState, useEffect } from 'react';
2import { useNavigate, useParams } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import { apiFetch } from '../lib/api';
5import { isAdmin } from '../utils/auth';
6import { notifySuccess, notifyError } from '../utils/notifications';
7import PayInvoiceModal from '../components/PayInvoiceModal';
9export default function InvoiceDetail() {
10 const { id } = useParams();
11 const navigate = useNavigate();
12 const [invoice, setInvoice] = useState(null);
13 const [loading, setLoading] = useState(true);
14 const [error, setError] = useState(null);
15 const [customer, setCustomer] = useState(null);
16 const [showPayModal, setShowPayModal] = useState(false);
17 const [pdfBlob, setPdfBlob] = useState(null);
18 const [pdfLoading, setPdfLoading] = useState(true);
19 const [companyInfo, setCompanyInfo] = useState({
27 const voidInvoice = async () => {
29 if (!confirm(`Void invoice #${invoice.invoice_id}? This will restock items and mark it as void.`)) return;
30 const reason = window.prompt('Enter a reason for voiding this invoice (optional):', '') || '';
32 const token = localStorage.getItem('token');
33 const res = await apiFetch(`/invoices/${invoice.invoice_id}/void`, {
35 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
36 body: JSON.stringify({ reason })
39 const txt = await res.text();
40 const errorMsg = txt || 'Failed to void invoice';
41 await notifyError('Void Failed', errorMsg);
42 throw new Error(errorMsg);
45 await notifySuccess('Invoice Voided', `Invoice #${invoice.invoice_id} has been voided successfully`);
47 // Error already notified above
51 const deleteInvoice = async () => {
53 if (!confirm(`Delete invoice #${invoice.invoice_id}? This will permanently remove it.`)) return;
55 const token = localStorage.getItem('token');
56 const res = await apiFetch(`/invoices/${invoice.invoice_id}`, {
58 headers: { Authorization: `Bearer ${token}` }
61 const txt = await res.text();
62 const errorMsg = txt || 'Failed to delete invoice';
63 await notifyError('Delete Failed', errorMsg);
64 throw new Error(errorMsg);
66 await notifySuccess('Invoice Deleted', `Invoice #${invoice.invoice_id} has been deleted successfully`);
67 navigate('/invoices');
69 // Error already notified above
79 // Cleanup blob URL on unmount
82 if (pdfBlob && typeof pdfBlob === 'string' && pdfBlob.startsWith('blob:')) {
83 URL.revokeObjectURL(pdfBlob);
88 const fetchPdfBlob = async () => {
91 // Revoke old blob URL if it exists
92 if (pdfBlob && typeof pdfBlob === 'string' && pdfBlob.startsWith('blob:')) {
93 URL.revokeObjectURL(pdfBlob);
96 const token = localStorage.getItem('token');
97 const baseURL = import.meta.env.VITE_API_BASE_URL || 'https://rmm-psa-backend-t9f7k.ondigitalocean.app/api';
98 const response = await fetch(`${baseURL}/invoices/${id}/pdf?t=${Date.now()}`, {
100 'Authorization': `Bearer ${token}`
102 credentials: 'include' // Include cookies
106 throw new Error('Failed to fetch PDF');
109 const blob = await response.blob();
110 const blobUrl = URL.createObjectURL(blob);
113 console.error('Error fetching PDF:', err);
114 await notifyError('PDF Error', 'Failed to load invoice PDF');
116 setPdfLoading(false);
120 const fetchCompanyInfo = async () => {
122 const token = localStorage.getItem('token');
123 const [settingsRes, abnRes, addressRes, phoneRes, emailRes] = await Promise.all([
124 apiFetch('/settings', { headers: { Authorization: `Bearer ${token}` } }),
125 apiFetch('/settings/company_abn', { headers: { Authorization: `Bearer ${token}` } }),
126 apiFetch('/settings/company_address', { headers: { Authorization: `Bearer ${token}` } }),
127 apiFetch('/settings/company_phone', { headers: { Authorization: `Bearer ${token}` } }),
128 apiFetch('/settings/company_email', { headers: { Authorization: `Bearer ${token}` } })
131 const settings = await settingsRes.json();
132 const abn = abnRes.ok ? await abnRes.json() : { setting_value: '' };
133 const address = addressRes.ok ? await addressRes.json() : { setting_value: '' };
134 const phone = phoneRes.ok ? await phoneRes.json() : { setting_value: '' };
135 const email = emailRes.ok ? await emailRes.json() : { setting_value: '' };
138 name: settings?.general?.companyName || 'Your Company',
139 abn: abn.setting_value || '',
140 address: address.setting_value || '',
141 phone: phone.setting_value || '',
142 email: email.setting_value || ''
145 console.error('Failed to fetch company info:', err);
149 const fetchInvoice = async () => {
152 const token = localStorage.getItem('token');
153 const res = await apiFetch(`/invoices/${id}`, { headers: { Authorization: `Bearer ${token}` } });
154 if (!res.ok) throw new Error('Failed to load invoice');
155 const data = await res.json();
158 // Fetch customer details
159 if (data.customer_id) {
160 const custRes = await apiFetch(`/customers/${data.customer_id}`, { headers: { Authorization: `Bearer ${token}` } });
162 const customerData = await custRes.json();
163 // Backend returns { customer: {...} } so extract the customer object
164 setCustomer(customerData.customer || customerData);
167 } catch (err) { setError(err.message || 'Error'); }
168 finally { setLoading(false); }
171 if (loading) return (<MainLayout><div className="page-content">Loading...</div></MainLayout>);
172 if (error) return (<MainLayout><div className="page-content">Error: {error}</div></MainLayout>);
173 if (!invoice) return (<MainLayout><div className="page-content">Invoice not found</div></MainLayout>);
177 <div className="page-content" style={{ maxWidth: '1200px', margin: '0 auto' }}>
179 {/* Action Buttons at Top */}
180 <div style={{ marginBottom: '20px', display: 'flex', gap: '12px', justifyContent: 'space-between', alignItems: 'center' }}>
182 <h2 style={{ margin: 0 }}>Invoice #{String(invoice.invoice_id).padStart(5, '0')}</h2>
184 display: 'inline-block',
187 borderRadius: '12px',
190 backgroundColor: invoice.payment_status === 'paid' ? '#d4edda' : invoice.status === 'void' ? '#e2e3e5' : '#fff3cd',
191 color: invoice.payment_status === 'paid' ? '#155724' : invoice.status === 'void' ? '#383d41' : '#856404'
193 {invoice.payment_status === 'paid' ? 'PAID' : invoice.status?.toUpperCase() || 'DRAFT'}
197 <div style={{ display: 'flex', gap: '12px' }}>
198 {/* Pay Now button - only show for unpaid invoices */}
199 {invoice.payment_status !== 'paid' && invoice.status !== 'void' && (
202 onClick={() => setShowPayModal(true)}
203 style={{ background: '#10b981', color: 'white' }}
205 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: '4px' }}>payments</span>
210 <button className="btn primary" onClick={() => navigate(`/invoices/${invoice.invoice_id}/edit`)}>
211 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: '4px' }}>edit</span>
217 onClick={voidInvoice}
218 disabled={invoice.status === 'void' || invoice.payment_status === 'paid'}
220 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: '4px' }}>block</span>
227 onClick={deleteInvoice}
228 disabled={!(invoice.status === 'draft' || invoice.status === 'void') || invoice.payment_status === 'paid'}
230 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: '4px' }}>delete</span>
237 href={pdfBlob || '#'}
238 download={`invoice-${String(invoice.invoice_id).padStart(5, '0')}.pdf`}
243 notifyError('PDF Not Ready', 'PDF is still loading');
247 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: '4px' }}>download</span>
251 <button className="btn" onClick={() => navigate('/invoices')}>
252 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: '4px' }}>arrow_back</span>
262 boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
266 flexDirection: 'column'
269 <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
270 <p>Loading PDF...</p>
283 <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
284 <p>Failed to load PDF</p>
290 {/* Payment Modal */}
291 {showPayModal && invoice && (
294 onClose={() => setShowPayModal(false)}
296 fetchInvoice(); // Reload invoice to show updated payment status