sdfsdf
This commit is contained in:
parent
6ca98dabd2
commit
3f696cdfc3
|
|
@ -709,6 +709,16 @@ async function convertQuoteToInvoice(quoteId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoice Management - Same accordion pattern
|
// 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() {
|
async function loadInvoices() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/invoices');
|
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;
|
document.getElementById('invoice-customer').value = data.invoice.customer_id;
|
||||||
const dateOnly = data.invoice.invoice_date.split('T')[0];
|
const dateOnly = data.invoice.invoice_date.split('T')[0];
|
||||||
document.getElementById('invoice-date').value = dateOnly;
|
document.getElementById('invoice-date').value = dateOnly;
|
||||||
|
|
@ -788,6 +799,9 @@ async function openInvoiceModal(invoiceId = null) {
|
||||||
itemCounter = 0;
|
itemCounter = 0;
|
||||||
setDefaultDate();
|
setDefaultDate();
|
||||||
|
|
||||||
|
// Fetch next invoice number
|
||||||
|
fetchNextInvoiceNumber();
|
||||||
|
|
||||||
// Add one default item
|
// Add one default item
|
||||||
addInvoiceItem();
|
addInvoiceItem();
|
||||||
}
|
}
|
||||||
|
|
@ -1037,7 +1051,15 @@ async function handleInvoiceSubmit(e) {
|
||||||
return;
|
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 = {
|
const data = {
|
||||||
|
invoice_number: invoiceNumber,
|
||||||
customer_id: parseInt(document.getElementById('invoice-customer').value),
|
customer_id: parseInt(document.getElementById('invoice-customer').value),
|
||||||
invoice_date: document.getElementById('invoice-date').value,
|
invoice_date: document.getElementById('invoice-date').value,
|
||||||
terms: document.getElementById('invoice-terms').value,
|
terms: document.getElementById('invoice-terms').value,
|
||||||
|
|
@ -1060,7 +1082,8 @@ async function handleInvoiceSubmit(e) {
|
||||||
closeInvoiceModal();
|
closeInvoiceModal();
|
||||||
loadInvoices();
|
loadInvoices();
|
||||||
} else {
|
} else {
|
||||||
alert('Error saving invoice');
|
const error = await response.json();
|
||||||
|
alert(error.error || 'Error saving invoice');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="invoice-form" class="space-y-6">
|
<form id="invoice-form" class="space-y-6">
|
||||||
<div class="grid grid-cols-4 gap-4">
|
<div class="grid grid-cols-5 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Invoice #</label>
|
||||||
|
<input type="text" id="invoice-number" required pattern="[0-9]+"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
title="Must be a numeric value">
|
||||||
|
</div>
|
||||||
<div x-data="customerSearch('invoice')" class="relative">
|
<div x-data="customerSearch('invoice')" class="relative">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Customer</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Customer</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|
|
||||||
73
server.js
73
server.js
|
|
@ -108,19 +108,15 @@ async function getNextQuoteNumber() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNextInvoiceNumber() {
|
async function getNextInvoiceNumber() {
|
||||||
const year = new Date().getFullYear();
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
'SELECT invoice_number FROM invoices WHERE invoice_number LIKE $1 ORDER BY invoice_number DESC LIMIT 1',
|
'SELECT MAX(CAST(invoice_number AS INTEGER)) as max_number FROM invoices WHERE invoice_number ~ \'^[0-9]+$\''
|
||||||
[`${year}-%`]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0 || result.rows[0].max_number === null) {
|
||||||
return `${year}-001`;
|
return '110508';
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastNumber = parseInt(result.rows[0].invoice_number.split('-')[1]);
|
return String(parseInt(result.rows[0].max_number) + 1);
|
||||||
const nextNumber = String(lastNumber + 1).padStart(3, '0');
|
|
||||||
return `${year}-${nextNumber}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo endpoints
|
// 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) => {
|
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();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
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;
|
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) => {
|
app.put('/api/invoices/:id', async (req, res) => {
|
||||||
const { id } = req.params;
|
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();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
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;
|
let subtotal = 0;
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
|
@ -554,10 +593,10 @@ app.put('/api/invoices/:id', async (req, res) => {
|
||||||
const total = subtotal + tax_amount;
|
const total = subtotal + tax_amount;
|
||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5,
|
`UPDATE invoices SET invoice_number = $1, customer_id = $2, invoice_date = $3, terms = $4, auth_code = $5, tax_exempt = $6,
|
||||||
tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, updated_at = CURRENT_TIMESTAMP
|
tax_rate = $7, subtotal = $8, tax_amount = $9, total = $10, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $10`,
|
WHERE id = $11`,
|
||||||
[customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, id]
|
[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]);
|
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
|
// PDF Generation using templates and persistent browser
|
||||||
app.get('/api/quotes/:id/pdf', async (req, res) => {
|
app.get('/api/quotes/:id/pdf', async (req, res) => {
|
||||||
const { id } = req.params;
|
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
|
// HTML Debug Endpoints
|
||||||
app.get('/api/quotes/:id/html', async (req, res) => {
|
app.get('/api/quotes/:id/html', async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
border-bottom: 2px solid #333;
|
border-bottom: 2px solid #333;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
.bill-to-section {
|
.bill-to-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 30px 0 40px 0;
|
margin: 0px 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-to {
|
.bill-to {
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
.items-table td.rate,
|
.items-table td.rate,
|
||||||
.items-table td.amount {
|
.items-table td.amount {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 100px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-row td {
|
.footer-row td {
|
||||||
|
|
@ -220,11 +220,9 @@
|
||||||
<em>Providing IT Services and Support in South Texas Since 1996</em>
|
<em>Providing IT Services and Support in South Texas Since 1996</em>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
Phone:<br>
|
|
||||||
(361) 765-8400<br>
|
(361) 765-8400<br>
|
||||||
(361) 765-8401<br>
|
(361) 765-8401<br>
|
||||||
(361) 232-6578<br>
|
(361) 232-6578<br>
|
||||||
Email:<br>
|
|
||||||
accounting@bayarea-cc.com
|
accounting@bayarea-cc.com
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
border-bottom: 2px solid #333;
|
border-bottom: 2px solid #333;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
.bill-to-section {
|
.bill-to-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 30px 0 60px 0;
|
margin: 0px 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-to {
|
.bill-to {
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
.items-table td.rate,
|
.items-table td.rate,
|
||||||
.items-table td.amount {
|
.items-table td.amount {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 100px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-row td {
|
.footer-row td {
|
||||||
|
|
@ -220,11 +220,9 @@
|
||||||
<em>Providing IT Services and Support in South Texas Since 1996</em>
|
<em>Providing IT Services and Support in South Texas Since 1996</em>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
Phone:<br>
|
|
||||||
(361) 765-8400<br>
|
(361) 765-8400<br>
|
||||||
(361) 765-8401<br>
|
(361) 765-8401<br>
|
||||||
(361) 232-6578<br>
|
(361) 232-6578<br>
|
||||||
Email:<br>
|
|
||||||
support@bayarea-cc.com
|
support@bayarea-cc.com
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue