sent date
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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(' ');
|
||||||
|
sendDateDisplay = `<span title="All send dates: ${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
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user