diff --git a/public/app.js b/public/app.js index 7f20945..b0e4da8 100644 --- a/public/app.js +++ b/public/app.js @@ -709,6 +709,16 @@ async function convertQuoteToInvoice(quoteId) { } // Invoice Management - Same accordion pattern +async function fetchNextInvoiceNumber() { + try { + const response = await fetch('/api/invoices/next-number'); + const data = await response.json(); + document.getElementById('invoice-number').value = data.next_number; + } catch (error) { + console.error('Error fetching next invoice number:', error); + } +} + async function loadInvoices() { try { const response = await fetch('/api/invoices'); @@ -765,6 +775,7 @@ async function openInvoiceModal(invoiceId = null) { } } + document.getElementById('invoice-number').value = data.invoice.invoice_number; document.getElementById('invoice-customer').value = data.invoice.customer_id; const dateOnly = data.invoice.invoice_date.split('T')[0]; document.getElementById('invoice-date').value = dateOnly; @@ -788,6 +799,9 @@ async function openInvoiceModal(invoiceId = null) { itemCounter = 0; setDefaultDate(); + // Fetch next invoice number + fetchNextInvoiceNumber(); + // Add one default item addInvoiceItem(); } @@ -1037,7 +1051,15 @@ async function handleInvoiceSubmit(e) { return; } + const invoiceNumber = document.getElementById('invoice-number').value; + + if (!invoiceNumber || !/^\d+$/.test(invoiceNumber)) { + alert('Invalid invoice number. Must be a numeric value.'); + return; + } + const data = { + invoice_number: invoiceNumber, customer_id: parseInt(document.getElementById('invoice-customer').value), invoice_date: document.getElementById('invoice-date').value, terms: document.getElementById('invoice-terms').value, @@ -1060,7 +1082,8 @@ async function handleInvoiceSubmit(e) { closeInvoiceModal(); loadInvoices(); } else { - alert('Error saving invoice'); + const error = await response.json(); + alert(error.error || 'Error saving invoice'); } } catch (error) { console.error('Error:', error); @@ -1090,4 +1113,4 @@ async function deleteInvoice(id) { function viewInvoicePDF(id) { window.open(`/api/invoices/${id}/pdf`, '_blank'); -} +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index a8d2d6f..1e985d5 100644 --- a/public/index.html +++ b/public/index.html @@ -335,7 +335,13 @@
-
+
+
+ + +
diff --git a/server.js b/server.js index 61d3265..a41c61c 100644 --- a/server.js +++ b/server.js @@ -108,19 +108,15 @@ async function getNextQuoteNumber() { } async function getNextInvoiceNumber() { - const year = new Date().getFullYear(); const result = await pool.query( - 'SELECT invoice_number FROM invoices WHERE invoice_number LIKE $1 ORDER BY invoice_number DESC LIMIT 1', - [`${year}-%`] + 'SELECT MAX(CAST(invoice_number AS INTEGER)) as max_number FROM invoices WHERE invoice_number ~ \'^[0-9]+$\'' ); - if (result.rows.length === 0) { - return `${year}-001`; + if (result.rows.length === 0 || result.rows[0].max_number === null) { + return '110508'; } - const lastNumber = parseInt(result.rows[0].invoice_number.split('-')[1]); - const nextNumber = String(lastNumber + 1).padStart(3, '0'); - return `${year}-${nextNumber}`; + return String(parseInt(result.rows[0].max_number) + 1); } // Logo endpoints @@ -419,14 +415,40 @@ app.get('/api/invoices/:id', async (req, res) => { } }); +// New endpoint to get next invoice number +app.get('/api/invoices/next-number', async (req, res) => { + try { + const nextNumber = await getNextInvoiceNumber(); + res.json({ next_number: nextNumber }); + } catch (error) { + console.error('Error getting next invoice number:', error); + res.status(500).json({ error: 'Error getting next invoice number' }); + } +}); + app.post('/api/invoices', async (req, res) => { - const { customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id } = req.body; + const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); - const invoice_number = await getNextInvoiceNumber(); + // Validate invoice_number is provided and is numeric + if (!invoice_number || !/^\d+$/.test(invoice_number)) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: 'Invalid invoice number. Must be a numeric value.' }); + } + + // Check if invoice number already exists + const existingInvoice = await client.query( + 'SELECT id FROM invoices WHERE invoice_number = $1', + [invoice_number] + ); + + if (existingInvoice.rows.length > 0) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` }); + } let subtotal = 0; @@ -534,12 +556,29 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { app.put('/api/invoices/:id', async (req, res) => { const { id } = req.params; - const { customer_id, invoice_date, terms, auth_code, tax_exempt, items } = req.body; + const { invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, items } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); + // Validate invoice_number is provided and is numeric + if (!invoice_number || !/^\d+$/.test(invoice_number)) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: 'Invalid invoice number. Must be a numeric value.' }); + } + + // Check if invoice number already exists (excluding current invoice) + const existingInvoice = await client.query( + 'SELECT id FROM invoices WHERE invoice_number = $1 AND id != $2', + [invoice_number, id] + ); + + if (existingInvoice.rows.length > 0) { + await client.query('ROLLBACK'); + return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` }); + } + let subtotal = 0; for (const item of items) { @@ -554,10 +593,10 @@ app.put('/api/invoices/:id', async (req, res) => { const total = subtotal + tax_amount; await client.query( - `UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5, - tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, updated_at = CURRENT_TIMESTAMP - WHERE id = $10`, - [customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, id] + `UPDATE invoices SET invoice_number = $1, customer_id = $2, invoice_date = $3, terms = $4, auth_code = $5, tax_exempt = $6, + tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, updated_at = CURRENT_TIMESTAMP + WHERE id = $11`, + [invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, id] ); await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); @@ -598,6 +637,8 @@ app.delete('/api/invoices/:id', async (req, res) => { } }); +// PDF Generation code continues below... + // PDF Generation using templates and persistent browser app.get('/api/quotes/:id/pdf', async (req, res) => { const { id } = req.params; @@ -858,8 +899,6 @@ app.get('/api/invoices/:id/pdf', async (req, res) => { }); -// Nach den PDF-Endpoints, vor "Start server", einfügen: - // HTML Debug Endpoints app.get('/api/quotes/:id/html', async (req, res) => { const { id } = req.params; diff --git a/templates/invoice-template.html b/templates/invoice-template.html index f23791a..f132d72 100644 --- a/templates/invoice-template.html +++ b/templates/invoice-template.html @@ -26,7 +26,7 @@ display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 40px; + margin-bottom: 20px; padding-bottom: 20px; border-bottom: 2px solid #333; position: relative; @@ -70,7 +70,7 @@ .bill-to-section { display: flex; justify-content: space-between; - margin: 30px 0 40px 0; + margin: 0px 0 20px 0; } .bill-to { @@ -166,7 +166,7 @@ .items-table td.rate, .items-table td.amount { text-align: right; - width: 100px; + width: 70px; } .footer-row td { @@ -220,11 +220,9 @@ Providing IT Services and Support in South Texas Since 1996
- Phone:
(361) 765-8400
(361) 765-8401
(361) 232-6578
- Email:
accounting@bayarea-cc.com
diff --git a/templates/quote-template.html b/templates/quote-template.html index feb9ce4..312ccaa 100644 --- a/templates/quote-template.html +++ b/templates/quote-template.html @@ -26,7 +26,7 @@ display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 40px; + margin-bottom: 20px; padding-bottom: 20px; border-bottom: 2px solid #333; position: relative; @@ -70,7 +70,7 @@ .bill-to-section { display: flex; justify-content: space-between; - margin: 30px 0 60px 0; + margin: 0px 0 20px 0; } .bill-to { @@ -166,7 +166,7 @@ .items-table td.rate, .items-table td.amount { text-align: right; - width: 100px; + width: 70px; } .footer-row td { @@ -220,11 +220,9 @@ Providing IT Services and Support in South Texas Since 1996
- Phone:
(361) 765-8400
(361) 765-8401
(361) 232-6578
- Email:
support@bayarea-cc.com