sent date

This commit is contained in:
2026-03-25 17:35:30 -05:00
parent 453f8654b7
commit a9173acc8d
3 changed files with 117 additions and 9 deletions

View File

@@ -73,7 +73,12 @@ const API = {
body: JSON.stringify({ status }) body: JSON.stringify({ status })
}).then(r => r.json()), }).then(r => r.json()),
getPdf: (id) => window.open(`/api/invoices/${id}/pdf`, '_blank'), 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 // Payment API

View File

@@ -218,14 +218,28 @@ function renderInvoiceRow(invoice) {
statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-200 text-orange-800">Open</span>`; statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-200 text-orange-800">Open</span>`;
} }
// Send Date // Send Date — show actual sent dates if available, otherwise scheduled
let sendDateDisplay = '—'; 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('&#10;');
sendDateDisplay = `<span title="All send dates:&#10;${allDates}" class="cursor-help border-b border-dotted border-gray-400">${formatDate(lastSent)}</span>`;
sendDateDisplay += ` <span class="text-xs text-gray-400">(${sentDates.length}x)</span>`;
}
} else if (invoice.scheduled_send_date) {
// No actual sends yet — show scheduled date with indicators
const sendDate = parseLocalDate(invoice.scheduled_send_date); const sendDate = parseLocalDate(invoice.scheduled_send_date);
const today = new Date(); today.setHours(0, 0, 0, 0); const today = new Date(); today.setHours(0, 0, 0, 0);
const daysUntil = Math.floor((sendDate - today) / 86400000); const daysUntil = Math.floor((sendDate - today) / 86400000);
sendDateDisplay = formatDate(invoice.scheduled_send_date); sendDateDisplay = formatDate(invoice.scheduled_send_date);
if (!paid && invoice.email_status !== 'sent') { if (!paid && invoice.email_status !== 'sent' && !overdue) {
if (daysUntil < 0) sendDateDisplay += ` <span class="text-xs text-red-500">(${Math.abs(daysUntil)}d ago)</span>`; if (daysUntil < 0) sendDateDisplay += ` <span class="text-xs text-red-500">(${Math.abs(daysUntil)}d ago)</span>`;
else if (daysUntil === 0) sendDateDisplay += ` <span class="text-xs text-orange-500 font-semibold">(today)</span>`; else if (daysUntil === 0) sendDateDisplay += ` <span class="text-xs text-orange-500 font-semibold">(today)</span>`;
else if (daysUntil <= 3) sendDateDisplay += ` <span class="text-xs text-yellow-600">(in ${daysUntil}d)</span>`; else if (daysUntil <= 3) sendDateDisplay += ` <span class="text-xs text-yellow-600">(in ${daysUntil}d)</span>`;
@@ -287,8 +301,11 @@ function renderInvoiceRow(invoice) {
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${invNumDisplay} ${statusBadge}</td> <td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${invNumDisplay} ${statusBadge}</td>
<td class="px-4 py-3 text-sm text-gray-500">${invoice.customer_name || 'N/A'}</td> <td class="px-4 py-3 text-sm text-gray-500">${invoice.customer_name || 'N/A'}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${sendDateDisplay}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td> <span class="cursor-pointer hover:text-blue-600" onclick="window.invoiceView.editSentDates(${invoice.id})" title="Click to edit sent dates">
${sendDateDisplay}
</span>
</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">${amountDisplay}</td> <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">${amountDisplay}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium space-x-1"> <td class="px-4 py-3 whitespace-nowrap text-sm font-medium space-x-1">
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${stripeEmailBtn} ${stripeCheckBtn} ${paidBtn} ${delBtn} ${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${sendBtn} ${stripeEmailBtn} ${stripeCheckBtn} ${paidBtn} ${delBtn}
@@ -570,11 +587,59 @@ async function checkStripePayment(invoiceId) {
if (typeof hideSpinner === 'function') hideSpinner(); 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 // Expose
// ============================================================ // ============================================================
window.invoiceView = { window.invoiceView = {
viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove, viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove,
loadInvoices, renderInvoiceView, setStatus, checkStripePayment loadInvoices, renderInvoiceView, setStatus, checkStripePayment, editSentDates
}; };

View File

@@ -894,8 +894,15 @@ router.post('/:id/send-email', async (req, res) => {
const stripeLink = invoice.stripe_payment_link_url || null; const stripeLink = invoice.stripe_payment_link_url || null;
const info = await sendInvoiceEmail(invoice, recipientEmail, customText, stripeLink, pdfBuffer); const info = await sendInvoiceEmail(invoice, recipientEmail, customText, stripeLink, pdfBuffer);
// 4. (Optional) Status in der DB aktualisieren // 4. Status in der DB aktualisieren
await pool.query('UPDATE invoices SET email_status = $1 WHERE id = $2', ['sent', id]); 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 }); res.json({ success: true, messageId: info.messageId });
@@ -1184,4 +1191,35 @@ async function recordStripePaymentInQbo(invoice, amount, methodLabel, stripeFee,
feeBooked: stripeFee > 0 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; module.exports = router;