diff --git a/public/js/utils/api.js b/public/js/utils/api.js index 42ffc39..47235a4 100644 --- a/public/js/utils/api.js +++ b/public/js/utils/api.js @@ -73,7 +73,12 @@ const API = { body: JSON.stringify({ status }) }).then(r => r.json()), getPdf: (id) => window.open(`/api/invoices/${id}/pdf`, '_blank'), - getHtml: (id) => window.open(`/api/invoices/${id}/html`, '_blank') + getHtml: (id) => window.open(`/api/invoices/${id}/html`, '_blank'), + updateSentDates: (id, dates) => fetch(`/api/invoices/${id}/sent-dates`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sent_dates: dates }) + }).then(r => r.json()) }, // Payment API diff --git a/public/js/views/invoice-view.js b/public/js/views/invoice-view.js index 109959b..d6cca3c 100644 --- a/public/js/views/invoice-view.js +++ b/public/js/views/invoice-view.js @@ -218,14 +218,28 @@ function renderInvoiceRow(invoice) { statusBadge = `Open`; } - // Send Date + // Send Date — show actual sent dates if available, otherwise scheduled let sendDateDisplay = '—'; - if (invoice.scheduled_send_date) { + const sentDates = invoice.sent_dates || []; + + if (sentDates.length > 0) { + // Show most recent sent date + const lastSent = sentDates[sentDates.length - 1]; + sendDateDisplay = formatDate(lastSent); + + if (sentDates.length > 1) { + // Tooltip with all dates + const allDates = sentDates.map(d => formatDate(d)).join(' '); + sendDateDisplay = `${formatDate(lastSent)}`; + sendDateDisplay += ` (${sentDates.length}x)`; + } + } else if (invoice.scheduled_send_date) { + // No actual sends yet — show scheduled date with indicators const sendDate = parseLocalDate(invoice.scheduled_send_date); const today = new Date(); today.setHours(0, 0, 0, 0); const daysUntil = Math.floor((sendDate - today) / 86400000); sendDateDisplay = formatDate(invoice.scheduled_send_date); - if (!paid && invoice.email_status !== 'sent') { + if (!paid && invoice.email_status !== 'sent' && !overdue) { if (daysUntil < 0) sendDateDisplay += ` (${Math.abs(daysUntil)}d ago)`; else if (daysUntil === 0) sendDateDisplay += ` (today)`; else if (daysUntil <= 3) sendDateDisplay += ` (in ${daysUntil}d)`; @@ -287,8 +301,11 @@ function renderInvoiceRow(invoice) { ${invNumDisplay} ${statusBadge} ${invoice.customer_name || 'N/A'} ${formatDate(invoice.invoice_date)} - ${sendDateDisplay} - ${invoice.terms} + + + ${sendDateDisplay} + + ${invoice.terms} ${amountDisplay} ${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${stripeEmailBtn} ${stripeCheckBtn} ${paidBtn} ${delBtn} @@ -570,11 +587,59 @@ async function checkStripePayment(invoiceId) { if (typeof hideSpinner === 'function') hideSpinner(); } } + +async function editSentDates(invoiceId) { + // Load current invoice data + const res = await fetch(`/api/invoices/${invoiceId}`); + const data = await res.json(); + const invoice = data.invoice; + const sentDates = invoice.sent_dates || []; + + // Build a simple prompt-based editor + let datesStr = sentDates.join('\n'); + const input = prompt( + 'Edit sent dates (one per line, YYYY-MM-DD format):\n\n' + + 'Add a new line to add a date.\nRemove a line to delete a date.\nLeave empty to clear all.', + datesStr + ); + + if (input === null) return; // Cancelled + + // Parse and validate + const newDates = input.trim() + ? input.trim().split('\n').map(d => d.trim()).filter(d => d) + : []; + + for (const d of newDates) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) { + alert(`Invalid date: "${d}"\nPlease use YYYY-MM-DD format.`); + return; + } + } + + try { + const response = await fetch(`/api/invoices/${invoiceId}/sent-dates`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sent_dates: newDates }) + }); + + if (response.ok) { + loadInvoices(); + } else { + const err = await response.json(); + alert(`Error: ${err.error}`); + } + } catch (e) { + console.error('Error updating sent dates:', e); + alert('Network error updating sent dates.'); + } +} // ============================================================ // Expose // ============================================================ window.invoiceView = { viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove, - loadInvoices, renderInvoiceView, setStatus, checkStripePayment + loadInvoices, renderInvoiceView, setStatus, checkStripePayment, editSentDates }; \ No newline at end of file diff --git a/src/routes/invoices.js b/src/routes/invoices.js index 192941e..492e2d1 100644 --- a/src/routes/invoices.js +++ b/src/routes/invoices.js @@ -894,8 +894,15 @@ router.post('/:id/send-email', async (req, res) => { const stripeLink = invoice.stripe_payment_link_url || null; const info = await sendInvoiceEmail(invoice, recipientEmail, customText, stripeLink, pdfBuffer); - // 4. (Optional) Status in der DB aktualisieren - await pool.query('UPDATE invoices SET email_status = $1 WHERE id = $2', ['sent', id]); + // 4. Status in der DB aktualisieren + await pool.query( + `UPDATE invoices + SET email_status = 'sent', + sent_dates = array_append(COALESCE(sent_dates, '{}'), CURRENT_DATE), + updated_at = CURRENT_TIMESTAMP + WHERE id = $1`, + [id] + ); res.json({ success: true, messageId: info.messageId }); @@ -1184,4 +1191,35 @@ async function recordStripePaymentInQbo(invoice, amount, methodLabel, stripeFee, feeBooked: stripeFee > 0 }; } +// PATCH update sent dates only +router.patch('/:id/sent-dates', async (req, res) => { + const { id } = req.params; + const { sent_dates } = req.body; + + if (!Array.isArray(sent_dates)) { + return res.status(400).json({ error: 'sent_dates must be an array of date strings.' }); + } + + // Validate each date + for (const d of sent_dates) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) { + return res.status(400).json({ error: `Invalid date format: ${d}. Expected YYYY-MM-DD.` }); + } + } + + try { + // Sort chronologically + const sorted = [...sent_dates].sort(); + + await pool.query( + 'UPDATE invoices SET sent_dates = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', + [sorted, id] + ); + + res.json({ success: true, sent_dates: sorted }); + } catch (error) { + console.error('Error updating sent dates:', error); + res.status(500).json({ error: 'Failed to update sent dates.' }); + } +}); module.exports = router;