update
This commit is contained in:
parent
a0c62d639e
commit
2bb304babe
|
|
@ -1,13 +1,13 @@
|
||||||
// invoice-view.js — ES Module für die Invoice View
|
// invoice-view.js — ES Module für die Invoice View
|
||||||
// Features: Status Filter, Customer Filter, Group by, Send Date, Mark Paid, Reset QBO
|
// v3: UTC date fix, Draft filter, persistent settings, PDF disabled for drafts
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State
|
// State
|
||||||
// ============================================================
|
// ============================================================
|
||||||
let invoices = [];
|
let invoices = [];
|
||||||
let filterCustomer = '';
|
let filterCustomer = localStorage.getItem('inv_filterCustomer') || '';
|
||||||
let filterStatus = 'unpaid'; // 'all' | 'unpaid' | 'paid' | 'overdue'
|
let filterStatus = localStorage.getItem('inv_filterStatus') || 'unpaid';
|
||||||
let groupBy = 'none'; // 'none' | 'week' | 'month'
|
let groupBy = localStorage.getItem('inv_groupBy') || 'none';
|
||||||
|
|
||||||
const OVERDUE_DAYS = 30;
|
const OVERDUE_DAYS = 30;
|
||||||
|
|
||||||
|
|
@ -15,9 +15,23 @@ const OVERDUE_DAYS = 30;
|
||||||
// Helpers
|
// Helpers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// KRITISCHER FIX: Datum-String 'YYYY-MM-DD' OHNE Timezone-Conversion parsen
|
||||||
|
// new Date('2026-02-16') interpretiert als UTC → in CST wird's der 15.
|
||||||
|
// Stattdessen: manuell parsen oder 'T12:00:00' anhängen
|
||||||
|
function parseLocalDate(dateStr) {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
// Wenn es schon ein T enthält (ISO mit Zeit), den Datumsteil nehmen
|
||||||
|
const str = String(dateStr).split('T')[0];
|
||||||
|
const parts = str.split('-');
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
// Monat ist 0-basiert in JS
|
||||||
|
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(date) {
|
function formatDate(date) {
|
||||||
if (!date) return '—';
|
if (!date) return '—';
|
||||||
const d = new Date(date);
|
const d = parseLocalDate(date);
|
||||||
|
if (!d) return '—';
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(d.getDate()).padStart(2, '0');
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
|
|
@ -25,19 +39,23 @@ function formatDate(date) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function daysSince(date) {
|
function daysSince(date) {
|
||||||
const d = new Date(date);
|
const d = parseLocalDate(date);
|
||||||
|
if (!d) return 0;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
return Math.floor((now - d) / 86400000);
|
return Math.floor((now - d) / 86400000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWeekNumber(date) {
|
function getWeekNumber(date) {
|
||||||
const d = new Date(date);
|
const d = parseLocalDate(date);
|
||||||
d.setHours(0, 0, 0, 0);
|
if (!d) return { year: 0, week: 0 };
|
||||||
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
|
const copy = new Date(d.getTime());
|
||||||
const week1 = new Date(d.getFullYear(), 0, 4);
|
copy.setHours(0, 0, 0, 0);
|
||||||
|
copy.setDate(copy.getDate() + 3 - ((copy.getDay() + 6) % 7));
|
||||||
|
const week1 = new Date(copy.getFullYear(), 0, 4);
|
||||||
return {
|
return {
|
||||||
year: d.getFullYear(),
|
year: copy.getFullYear(),
|
||||||
week: 1 + Math.round(((d - week1) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7)
|
week: 1 + Math.round(((copy - week1) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +66,12 @@ function getWeekRange(year, weekNum) {
|
||||||
monday.setDate(jan4.getDate() - dayOfWeek + 1 + (weekNum - 1) * 7);
|
monday.setDate(jan4.getDate() - dayOfWeek + 1 + (weekNum - 1) * 7);
|
||||||
const sunday = new Date(monday);
|
const sunday = new Date(monday);
|
||||||
sunday.setDate(monday.getDate() + 6);
|
sunday.setDate(monday.getDate() + 6);
|
||||||
return { start: formatDate(monday), end: formatDate(sunday) };
|
const fmt = (d) => {
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dy = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${m}/${dy}/${d.getFullYear()}`;
|
||||||
|
};
|
||||||
|
return { start: fmt(monday), end: fmt(sunday) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMonthName(monthIndex) {
|
function getMonthName(monthIndex) {
|
||||||
|
|
@ -60,10 +83,21 @@ function isPaid(inv) {
|
||||||
return !!inv.paid_date;
|
return !!inv.paid_date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDraft(inv) {
|
||||||
|
return !inv.qbo_id;
|
||||||
|
}
|
||||||
|
|
||||||
function isOverdue(inv) {
|
function isOverdue(inv) {
|
||||||
return !isPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS;
|
return !isPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save state to localStorage
|
||||||
|
function saveSettings() {
|
||||||
|
localStorage.setItem('inv_filterStatus', filterStatus);
|
||||||
|
localStorage.setItem('inv_groupBy', groupBy);
|
||||||
|
localStorage.setItem('inv_filterCustomer', filterCustomer);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Data Loading
|
// Data Loading
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -96,6 +130,8 @@ function getFilteredInvoices() {
|
||||||
filtered = filtered.filter(inv => isPaid(inv));
|
filtered = filtered.filter(inv => isPaid(inv));
|
||||||
} else if (filterStatus === 'overdue') {
|
} else if (filterStatus === 'overdue') {
|
||||||
filtered = filtered.filter(inv => isOverdue(inv));
|
filtered = filtered.filter(inv => isOverdue(inv));
|
||||||
|
} else if (filterStatus === 'draft') {
|
||||||
|
filtered = filtered.filter(inv => isDraft(inv) && !isPaid(inv));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customer Filter
|
// Customer Filter
|
||||||
|
|
@ -107,7 +143,11 @@ function getFilteredInvoices() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sortierung: neueste zuerst
|
// Sortierung: neueste zuerst
|
||||||
filtered.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date));
|
filtered.sort((a, b) => {
|
||||||
|
const da = parseLocalDate(a.invoice_date);
|
||||||
|
const db = parseLocalDate(b.invoice_date);
|
||||||
|
return (db || 0) - (da || 0);
|
||||||
|
});
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +158,8 @@ function groupInvoices(filtered) {
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|
||||||
filtered.forEach(inv => {
|
filtered.forEach(inv => {
|
||||||
const d = new Date(inv.invoice_date);
|
const d = parseLocalDate(inv.invoice_date);
|
||||||
|
if (!d) return;
|
||||||
let key, label;
|
let key, label;
|
||||||
|
|
||||||
if (groupBy === 'week') {
|
if (groupBy === 'week') {
|
||||||
|
|
@ -142,7 +183,11 @@ function groupInvoices(filtered) {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const group of groups.values()) {
|
for (const group of groups.values()) {
|
||||||
group.invoices.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date));
|
group.invoices.sort((a, b) => {
|
||||||
|
const da = parseLocalDate(a.invoice_date);
|
||||||
|
const db = parseLocalDate(b.invoice_date);
|
||||||
|
return (db || 0) - (da || 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Map([...groups.entries()].sort((a, b) => b[0].localeCompare(a[0])));
|
return new Map([...groups.entries()].sort((a, b) => b[0].localeCompare(a[0])));
|
||||||
|
|
@ -156,22 +201,13 @@ function renderInvoiceRow(invoice) {
|
||||||
const hasQbo = !!invoice.qbo_id;
|
const hasQbo = !!invoice.qbo_id;
|
||||||
const paid = isPaid(invoice);
|
const paid = isPaid(invoice);
|
||||||
const overdue = isOverdue(invoice);
|
const overdue = isOverdue(invoice);
|
||||||
|
const draft = isDraft(invoice);
|
||||||
|
|
||||||
// Invoice Number Display
|
// Invoice Number Display
|
||||||
const invNumDisplay = invoice.invoice_number
|
const invNumDisplay = invoice.invoice_number
|
||||||
? invoice.invoice_number
|
? invoice.invoice_number
|
||||||
: `<span class="text-gray-400 italic text-xs">Draft</span>`;
|
: `<span class="text-gray-400 italic text-xs">Draft</span>`;
|
||||||
|
|
||||||
// QBO Button — if already in QBO, show checkmark + optional reset
|
|
||||||
const qboButton = hasQbo
|
|
||||||
? `<span class="text-gray-400 text-xs cursor-pointer" title="In QBO (ID: ${invoice.qbo_id}) — Click to reset" onclick="window.invoiceView.resetQbo(${invoice.id})">✓ QBO</span>`
|
|
||||||
: `<button onclick="window.invoiceView.exportToQBO(${invoice.id})" class="text-orange-600 hover:text-orange-900" title="Export to QuickBooks">QBO Export</button>`;
|
|
||||||
|
|
||||||
// Paid/Unpaid Toggle
|
|
||||||
const paidButton = paid
|
|
||||||
? `<button onclick="window.invoiceView.markUnpaid(${invoice.id})" class="text-yellow-600 hover:text-yellow-800" title="Mark as unpaid">↩ Unpaid</button>`
|
|
||||||
: `<button onclick="window.invoiceView.markPaid(${invoice.id})" class="text-emerald-600 hover:text-emerald-800" title="Mark as paid">💰 Paid</button>`;
|
|
||||||
|
|
||||||
// Status Badge
|
// Status Badge
|
||||||
let statusBadge = '';
|
let statusBadge = '';
|
||||||
if (paid) {
|
if (paid) {
|
||||||
|
|
@ -183,10 +219,9 @@ function renderInvoiceRow(invoice) {
|
||||||
// Send Date display
|
// Send Date display
|
||||||
let sendDateDisplay = '—';
|
let sendDateDisplay = '—';
|
||||||
if (invoice.scheduled_send_date) {
|
if (invoice.scheduled_send_date) {
|
||||||
const sendDate = new Date(invoice.scheduled_send_date);
|
const sendDate = parseLocalDate(invoice.scheduled_send_date);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0,0,0,0);
|
today.setHours(0, 0, 0, 0);
|
||||||
sendDate.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);
|
||||||
|
|
@ -202,6 +237,30 @@ function renderInvoiceRow(invoice) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- ACTION BUTTONS (Reihenfolge: Edit | QBO | PDF HTML | Paid | Del) ---
|
||||||
|
|
||||||
|
// Edit
|
||||||
|
const editBtn = `<button onclick="window.invoiceView.edit(${invoice.id})" class="text-blue-600 hover:text-blue-900">Edit</button>`;
|
||||||
|
|
||||||
|
// QBO
|
||||||
|
const qboBtn = hasQbo
|
||||||
|
? `<span class="text-gray-400 text-xs cursor-pointer" title="In QBO (ID: ${invoice.qbo_id}) — Click to reset" onclick="window.invoiceView.resetQbo(${invoice.id})">✓ QBO</span>`
|
||||||
|
: `<button onclick="window.invoiceView.exportToQBO(${invoice.id})" class="text-orange-600 hover:text-orange-900" title="Export to QuickBooks">QBO Export</button>`;
|
||||||
|
|
||||||
|
// PDF + HTML — PDF deaktiviert wenn Draft (keine Rechnungsnummer)
|
||||||
|
const pdfBtn = draft
|
||||||
|
? `<span class="text-gray-300 text-sm cursor-not-allowed" title="PDF erst nach QBO Export verfügbar">PDF</span>`
|
||||||
|
: `<button onclick="window.invoiceView.viewPDF(${invoice.id})" class="text-green-600 hover:text-green-900">PDF</button>`;
|
||||||
|
const htmlBtn = `<button onclick="window.invoiceView.viewHTML(${invoice.id})" class="text-teal-600 hover:text-teal-900">HTML</button>`;
|
||||||
|
|
||||||
|
// Paid/Unpaid
|
||||||
|
const paidBtn = paid
|
||||||
|
? `<button onclick="window.invoiceView.markUnpaid(${invoice.id})" class="text-yellow-600 hover:text-yellow-800" title="Mark as unpaid">↩ Unpaid</button>`
|
||||||
|
: `<button onclick="window.invoiceView.markPaid(${invoice.id})" class="text-emerald-600 hover:text-emerald-800" title="Mark as paid">💰 Paid</button>`;
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
const delBtn = `<button onclick="window.invoiceView.remove(${invoice.id})" class="text-red-600 hover:text-red-900">Del</button>`;
|
||||||
|
|
||||||
const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : '';
|
const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|
@ -215,12 +274,7 @@ function renderInvoiceRow(invoice) {
|
||||||
<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-500">${invoice.terms}</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">$${parseFloat(invoice.total).toFixed(2)}</td>
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 font-semibold">$${parseFloat(invoice.total).toFixed(2)}</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">
|
||||||
<button onclick="window.invoiceView.viewPDF(${invoice.id})" class="text-green-600 hover:text-green-900">PDF</button>
|
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${paidBtn} ${delBtn}
|
||||||
<button onclick="window.invoiceView.viewHTML(${invoice.id})" class="text-teal-600 hover:text-teal-900">HTML</button>
|
|
||||||
${qboButton}
|
|
||||||
${paidButton}
|
|
||||||
<button onclick="window.invoiceView.edit(${invoice.id})" class="text-blue-600 hover:text-blue-900">Edit</button>
|
|
||||||
<button onclick="window.invoiceView.remove(${invoice.id})" class="text-red-600 hover:text-red-900">Del</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
@ -327,6 +381,17 @@ function updateStatusButtons() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draftCount = invoices.filter(inv => isDraft(inv) && !isPaid(inv)).length;
|
||||||
|
const draftBadge = document.getElementById('draft-badge');
|
||||||
|
if (draftBadge) {
|
||||||
|
if (draftCount > 0) {
|
||||||
|
draftBadge.textContent = draftCount;
|
||||||
|
draftBadge.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
draftBadge.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unpaidCount = invoices.filter(inv => !isPaid(inv)).length;
|
const unpaidCount = invoices.filter(inv => !isPaid(inv)).length;
|
||||||
const unpaidBadge = document.getElementById('unpaid-badge');
|
const unpaidBadge = document.getElementById('unpaid-badge');
|
||||||
if (unpaidBadge) {
|
if (unpaidBadge) {
|
||||||
|
|
@ -353,7 +418,7 @@ export function injectToolbar() {
|
||||||
</button>
|
</button>
|
||||||
<button data-status-filter="unpaid"
|
<button data-status-filter="unpaid"
|
||||||
onclick="window.invoiceView.setStatus('unpaid')"
|
onclick="window.invoiceView.setStatus('unpaid')"
|
||||||
class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-blue-600 text-white">
|
class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600">
|
||||||
Unpaid <span id="unpaid-badge" class="ml-1 text-xs opacity-80"></span>
|
Unpaid <span id="unpaid-badge" class="ml-1 text-xs opacity-80"></span>
|
||||||
</button>
|
</button>
|
||||||
<button data-status-filter="paid"
|
<button data-status-filter="paid"
|
||||||
|
|
@ -367,6 +432,12 @@ export function injectToolbar() {
|
||||||
Overdue
|
Overdue
|
||||||
<span id="overdue-badge" class="hidden absolute -top-1.5 -right-1.5 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">0</span>
|
<span id="overdue-badge" class="hidden absolute -top-1.5 -right-1.5 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">0</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button data-status-filter="draft"
|
||||||
|
onclick="window.invoiceView.setStatus('draft')"
|
||||||
|
class="relative px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600">
|
||||||
|
Draft
|
||||||
|
<span id="draft-badge" class="hidden absolute -top-1.5 -right-1.5 bg-gray-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">0</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-8 bg-gray-300"></div>
|
<div class="w-px h-8 bg-gray-300"></div>
|
||||||
|
|
@ -375,6 +446,7 @@ export function injectToolbar() {
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="text-sm font-medium text-gray-700">Customer:</label>
|
<label class="text-sm font-medium text-gray-700">Customer:</label>
|
||||||
<input type="text" id="invoice-filter-customer" placeholder="Filter by name..."
|
<input type="text" id="invoice-filter-customer" placeholder="Filter by name..."
|
||||||
|
value="${filterCustomer}"
|
||||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm w-48 focus:ring-blue-500 focus:border-blue-500">
|
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm w-48 focus:ring-blue-500 focus:border-blue-500">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -384,9 +456,9 @@ export function injectToolbar() {
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="text-sm font-medium text-gray-700">Group:</label>
|
<label class="text-sm font-medium text-gray-700">Group:</label>
|
||||||
<select id="invoice-group-by" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm bg-white focus:ring-blue-500 focus:border-blue-500">
|
<select id="invoice-group-by" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm bg-white focus:ring-blue-500 focus:border-blue-500">
|
||||||
<option value="none">None</option>
|
<option value="none" ${groupBy === 'none' ? 'selected' : ''}>None</option>
|
||||||
<option value="week">Week</option>
|
<option value="week" ${groupBy === 'week' ? 'selected' : ''}>Week</option>
|
||||||
<option value="month">Month</option>
|
<option value="month" ${groupBy === 'month' ? 'selected' : ''}>Month</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -397,13 +469,18 @@ export function injectToolbar() {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Restore active button state
|
||||||
|
updateStatusButtons();
|
||||||
|
|
||||||
document.getElementById('invoice-filter-customer').addEventListener('input', (e) => {
|
document.getElementById('invoice-filter-customer').addEventListener('input', (e) => {
|
||||||
filterCustomer = e.target.value;
|
filterCustomer = e.target.value;
|
||||||
|
saveSettings();
|
||||||
renderInvoiceView();
|
renderInvoiceView();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('invoice-group-by').addEventListener('change', (e) => {
|
document.getElementById('invoice-group-by').addEventListener('change', (e) => {
|
||||||
groupBy = e.target.value;
|
groupBy = e.target.value;
|
||||||
|
saveSettings();
|
||||||
renderInvoiceView();
|
renderInvoiceView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -414,6 +491,7 @@ export function injectToolbar() {
|
||||||
|
|
||||||
export function setStatus(status) {
|
export function setStatus(status) {
|
||||||
filterStatus = status;
|
filterStatus = status;
|
||||||
|
saveSettings();
|
||||||
renderInvoiceView();
|
renderInvoiceView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -893,7 +893,7 @@ app.get('/api/invoices/:id/pdf', async (req, res) => {
|
||||||
.replace('{{CUSTOMER_CITY}}', invoice.city || '')
|
.replace('{{CUSTOMER_CITY}}', invoice.city || '')
|
||||||
.replace('{{CUSTOMER_STATE}}', invoice.state || '')
|
.replace('{{CUSTOMER_STATE}}', invoice.state || '')
|
||||||
.replace('{{CUSTOMER_ZIP}}', invoice.zip_code || '')
|
.replace('{{CUSTOMER_ZIP}}', invoice.zip_code || '')
|
||||||
.replace('{{INVOICE_NUMBER}}', invoice.invoice_number)
|
.replace('{{INVOICE_NUMBER}}', invoice.invoice_number || '')
|
||||||
.replace('{{ACCOUNT_NUMBER}}', invoice.account_number || '')
|
.replace('{{ACCOUNT_NUMBER}}', invoice.account_number || '')
|
||||||
.replace('{{INVOICE_DATE}}', formatDate(invoice.invoice_date))
|
.replace('{{INVOICE_DATE}}', formatDate(invoice.invoice_date))
|
||||||
.replace('{{TERMS}}', invoice.terms)
|
.replace('{{TERMS}}', invoice.terms)
|
||||||
|
|
@ -1118,7 +1118,7 @@ app.get('/api/invoices/:id/html', async (req, res) => {
|
||||||
.replace('{{CUSTOMER_CITY}}', invoice.city || '')
|
.replace('{{CUSTOMER_CITY}}', invoice.city || '')
|
||||||
.replace('{{CUSTOMER_STATE}}', invoice.state || '')
|
.replace('{{CUSTOMER_STATE}}', invoice.state || '')
|
||||||
.replace('{{CUSTOMER_ZIP}}', invoice.zip_code || '')
|
.replace('{{CUSTOMER_ZIP}}', invoice.zip_code || '')
|
||||||
.replace('{{INVOICE_NUMBER}}', invoice.invoice_number)
|
.replace('{{INVOICE_NUMBER}}', invoice.invoice_number || '')
|
||||||
.replace('{{ACCOUNT_NUMBER}}', invoice.account_number || '')
|
.replace('{{ACCOUNT_NUMBER}}', invoice.account_number || '')
|
||||||
.replace('{{INVOICE_DATE}}', formatDate(invoice.invoice_date))
|
.replace('{{INVOICE_DATE}}', formatDate(invoice.invoice_date))
|
||||||
.replace('{{TERMS}}', invoice.terms)
|
.replace('{{TERMS}}', invoice.terms)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue