This commit is contained in:
Andreas Knuth 2026-02-17 16:51:30 -06:00
parent 31f03b0d7c
commit 2d5be21bf2
6 changed files with 633 additions and 66 deletions

View File

@ -78,7 +78,7 @@ window.customerSearch = customerSearch;
document.addEventListener('DOMContentLoaded', () => {
loadCustomers();
loadQuotes();
loadInvoices();
//loadInvoices();
setDefaultDate();
checkCurrentLogo();
@ -755,33 +755,37 @@ async function fetchNextInvoiceNumber() {
}
async function loadInvoices() {
// Wird vom invoice-view.js Modul überschrieben (window.loadInvoices)
// Dieser Fallback lädt die Daten falls das Modul noch nicht geladen ist
try {
const response = await fetch('/api/invoices');
invoices = await response.json();
// Falls das Modul geladen ist, nutze dessen Renderer
if (window.invoiceView) {
window.invoiceView.renderInvoiceView();
} else {
renderInvoices();
}
} catch (error) {
console.error('Error loading invoices:', error);
alert('Error loading invoices');
}
}
function renderInvoices() {
if (window.invoiceView) {
window.invoiceView.renderInvoiceView();
return;
}
// Minimaler Fallback falls Modul nicht geladen
const tbody = document.getElementById('invoices-list');
tbody.innerHTML = invoices.map(invoice => `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${invoice.invoice_number}</td>
<td class="px-6 py-4 text-sm font-medium text-gray-900">${invoice.invoice_number}</td>
<td class="px-6 py-4 text-sm text-gray-500">${invoice.customer_name || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">$${parseFloat(invoice.total).toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button onclick="viewInvoicePDF(${invoice.id})" class="text-green-600 hover:text-green-900">PDF</button>
<button onclick="exportToQBO(${invoice.id})" class="text-orange-600 hover:text-orange-900" title="Export to QuickBooks">
QBO Export
</button>
<button onclick="editInvoice(${invoice.id})" class="text-blue-600 hover:text-blue-900">Edit</button>
<button onclick="deleteInvoice(${invoice.id})" class="text-red-600 hover:text-red-900">Delete</button>
</td>
<td class="px-6 py-4 text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td>
<td class="px-6 py-4 text-sm text-gray-500">${invoice.terms}</td>
<td class="px-6 py-4 text-sm font-semibold">$${parseFloat(invoice.total).toFixed(2)}</td>
<td class="px-6 py-4 text-sm">Loading module...</td>
</tr>
`).join('');
}
@ -1125,38 +1129,13 @@ async function deleteInvoice(id) {
}
function viewInvoicePDF(id) {
window.open(`/api/invoices/${id}/pdf`, '_blank');
}
// *** FIX 2: Verbesserte Erfolgsmeldung mit QBO DocNumber ***
async function exportToQBO(id) {
if (!confirm('Rechnung wirklich an QuickBooks Online senden?')) return;
// UI Feedback (einfach, aber wirksam)
const btn = event.target; // Der geklickte Button
const originalText = btn.textContent;
btn.textContent = "⏳...";
btn.disabled = true;
try {
const response = await fetch(`/api/invoices/${id}/export`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
alert(`✅ Erfolg! QBO ID: ${result.qbo_id}, Rechnungsnr: ${result.qbo_doc_number}`);
// Liste neu laden um aktualisierte invoice_number anzuzeigen
loadInvoices();
if (window.invoiceView) {
window.invoiceView.viewPDF(id);
} else {
alert(`❌ Fehler: ${result.error}`);
}
} catch (error) {
console.error(error);
alert('Netzwerkfehler beim Export.');
} finally {
btn.textContent = originalText;
btn.disabled = false;
window.open(`/api/invoices/${id}/pdf`, '_blank');
}
}
async function checkQboOverdue() {
const btn = document.querySelector('button[onclick="checkQboOverdue()"]');
const resultDiv = document.getElementById('qbo-result');
@ -1263,3 +1242,4 @@ async function importFromQBO() {
btn.disabled = false;
}
}
window.openInvoiceModal = openInvoiceModal;

View File

@ -67,13 +67,16 @@
<!-- Invoices Tab -->
<div id="invoices-tab" class="tab-content hidden">
<div class="flex justify-between items-center mb-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-3xl font-bold text-gray-800">Invoices</h2>
<button onclick="openInvoiceModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold shadow-md">
+ New Invoice
</button>
</div>
<!-- Toolbar wird von invoice-view.js injiziert -->
<div id="invoice-toolbar"></div>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
@ -551,5 +554,6 @@
</div>
<script src="app.js"></script>
<script type="module" src="invoice-view-init.js"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
// invoice-view-init.js — Bootstrap-Script (type="module")
// Wird in index.html als <script type="module"> geladen.
// Importiert das Invoice-View Modul und verbindet es mit der bestehenden App.
import { loadInvoices, renderInvoiceView, injectToolbar } from './invoice-view.js';
// Warte bis DOM fertig ist
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
// Toolbar injizieren
injectToolbar();
// Globale Funktionen für app.js verfügbar machen
// (app.js ruft loadInvoices() auf wenn der Tab gewechselt wird)
window.loadInvoices = loadInvoices;
window.renderInvoices = renderInvoiceView;
// Initiales Laden
loadInvoices();
}

503
public/invoice-view.js Normal file
View File

@ -0,0 +1,503 @@
// invoice-view.js — ES Module für die Invoice View
// Features: Status Filter (all/unpaid/paid/overdue), Customer Filter,
// Group by (none/week/month), Sortierung neueste zuerst, Mark Paid/Unpaid
// ============================================================
// State
// ============================================================
let invoices = [];
let filterCustomer = '';
let filterStatus = 'unpaid'; // 'all' | 'unpaid' | 'paid' | 'overdue'
let groupBy = 'none'; // 'none' | 'week' | 'month'
const OVERDUE_DAYS = 30;
// ============================================================
// Helpers
// ============================================================
function formatDate(date) {
const d = new Date(date);
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const year = d.getFullYear();
return `${month}/${day}/${year}`;
}
function daysSince(date) {
const d = new Date(date);
const now = new Date();
return Math.floor((now - d) / 86400000);
}
function getWeekNumber(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
const week1 = new Date(d.getFullYear(), 0, 4);
return {
year: d.getFullYear(),
week: 1 + Math.round(((d - week1) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7)
};
}
function getWeekRange(year, weekNum) {
const jan4 = new Date(year, 0, 4);
const dayOfWeek = jan4.getDay() || 7;
const monday = new Date(jan4);
monday.setDate(jan4.getDate() - dayOfWeek + 1 + (weekNum - 1) * 7);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
return { start: formatDate(monday), end: formatDate(sunday) };
}
function getMonthName(monthIndex) {
return ['January','February','March','April','May','June',
'July','August','September','October','November','December'][monthIndex];
}
function isPaid(inv) {
return !!inv.paid_date;
}
function isOverdue(inv) {
return !isPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS;
}
// ============================================================
// Data Loading
// ============================================================
export async function loadInvoices() {
try {
const response = await fetch('/api/invoices');
invoices = await response.json();
renderInvoiceView();
} catch (error) {
console.error('Error loading invoices:', error);
}
}
export function getInvoicesData() {
return invoices;
}
// ============================================================
// Filtering & Sorting & Grouping
// ============================================================
function getFilteredInvoices() {
let filtered = [...invoices];
// Status Filter
if (filterStatus === 'unpaid') {
filtered = filtered.filter(inv => !isPaid(inv));
} else if (filterStatus === 'paid') {
filtered = filtered.filter(inv => isPaid(inv));
} else if (filterStatus === 'overdue') {
filtered = filtered.filter(inv => isOverdue(inv));
}
// 'all' → kein Filter
// Customer Filter
if (filterCustomer.trim()) {
const search = filterCustomer.toLowerCase();
filtered = filtered.filter(inv =>
(inv.customer_name || '').toLowerCase().includes(search)
);
}
// Sortierung: neueste zuerst
filtered.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date));
return filtered;
}
function groupInvoices(filtered) {
if (groupBy === 'none') return null;
const groups = new Map();
filtered.forEach(inv => {
const d = new Date(inv.invoice_date);
let key, label;
if (groupBy === 'week') {
const wk = getWeekNumber(inv.invoice_date);
key = `${wk.year}-W${String(wk.week).padStart(2, '0')}`;
const range = getWeekRange(wk.year, wk.week);
label = `Week ${wk.week}, ${wk.year} (${range.start} ${range.end})`;
} else if (groupBy === 'month') {
const month = d.getMonth();
const year = d.getFullYear();
key = `${year}-${String(month).padStart(2, '0')}`;
label = `${getMonthName(month)} ${year}`;
}
if (!groups.has(key)) {
groups.set(key, { label, invoices: [], total: 0 });
}
const group = groups.get(key);
group.invoices.push(inv);
group.total += parseFloat(inv.total) || 0;
});
// Innerhalb jeder Gruppe nochmal nach Datum sortieren (neueste zuerst)
for (const group of groups.values()) {
group.invoices.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date));
}
// Gruppen nach Key sortieren (neueste zuerst)
return new Map([...groups.entries()].sort((a, b) => b[0].localeCompare(a[0])));
}
// ============================================================
// Rendering
// ============================================================
function renderInvoiceRow(invoice) {
const hasQbo = !!invoice.qbo_id;
const paid = isPaid(invoice);
const overdue = isOverdue(invoice);
// QBO Button
const qboButton = hasQbo
? `<span class="text-gray-400 text-xs" title="Already in QBO (ID: ${invoice.qbo_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 Button
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
let statusBadge = '';
if (paid) {
statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800" title="Paid ${formatDate(invoice.paid_date)}">Paid</span>`;
} else if (overdue) {
statusBadge = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-red-100 text-red-800" title="${daysSince(invoice.invoice_date)} days old">Overdue</span>`;
}
// Row styling
const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : '';
return `
<tr class="${rowClass}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
${invoice.invoice_number} ${statusBadge}
</td>
<td class="px-6 py-4 text-sm text-gray-500">${invoice.customer_name || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatDate(invoice.invoice_date)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${invoice.terms}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">$${parseFloat(invoice.total).toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button onclick="window.invoiceView.viewPDF(${invoice.id})" class="text-green-600 hover:text-green-900">PDF</button>
<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">Delete</button>
</td>
</tr>
`;
}
function renderGroupHeader(label) {
return `
<tr class="bg-blue-50">
<td colspan="6" class="px-6 py-3 text-sm font-bold text-blue-800">
📅 ${label}
</td>
</tr>
`;
}
function renderGroupFooter(total, count) {
return `
<tr class="bg-gray-50 border-t-2 border-gray-300">
<td colspan="4" class="px-6 py-3 text-sm font-bold text-gray-700 text-right">Group Total (${count} invoices):</td>
<td class="px-6 py-3 text-sm font-bold text-gray-900">$${total.toFixed(2)}</td>
<td></td>
</tr>
`;
}
export function renderInvoiceView() {
const tbody = document.getElementById('invoices-list');
if (!tbody) return;
const filtered = getFilteredInvoices();
const groups = groupInvoices(filtered);
let html = '';
let grandTotal = 0;
if (groups) {
for (const [key, group] of groups) {
html += renderGroupHeader(group.label);
group.invoices.forEach(inv => {
html += renderInvoiceRow(inv);
});
html += renderGroupFooter(group.total, group.invoices.length);
grandTotal += group.total;
}
if (groups.size > 1) {
html += `
<tr class="bg-blue-100 border-t-4 border-blue-400">
<td colspan="4" class="px-6 py-4 text-base font-bold text-blue-900 text-right">Grand Total (${filtered.length} invoices):</td>
<td class="px-6 py-4 text-base font-bold text-blue-900">$${grandTotal.toFixed(2)}</td>
<td></td>
</tr>
`;
}
} else {
filtered.forEach(inv => {
html += renderInvoiceRow(inv);
grandTotal += parseFloat(inv.total) || 0;
});
if (filtered.length > 0) {
html += `
<tr class="bg-gray-100 border-t-2 border-gray-300">
<td colspan="4" class="px-6 py-3 text-sm font-bold text-gray-700 text-right">Total (${filtered.length} invoices):</td>
<td class="px-6 py-3 text-sm font-bold text-gray-900">$${grandTotal.toFixed(2)}</td>
<td></td>
</tr>
`;
}
}
if (filtered.length === 0) {
html = `<tr><td colspan="6" class="px-6 py-8 text-center text-gray-500">No invoices found.</td></tr>`;
}
tbody.innerHTML = html;
// Update count badge
const countEl = document.getElementById('invoice-count');
if (countEl) countEl.textContent = filtered.length;
// Update status button active states
updateStatusButtons();
}
function updateStatusButtons() {
document.querySelectorAll('[data-status-filter]').forEach(btn => {
const status = btn.getAttribute('data-status-filter');
if (status === filterStatus) {
btn.classList.remove('bg-white', 'text-gray-600');
btn.classList.add('bg-blue-600', 'text-white');
} else {
btn.classList.remove('bg-blue-600', 'text-white');
btn.classList.add('bg-white', 'text-gray-600');
}
});
// Update overdue count badge
const overdueCount = invoices.filter(inv => isOverdue(inv)).length;
const overdueBadge = document.getElementById('overdue-badge');
if (overdueBadge) {
if (overdueCount > 0) {
overdueBadge.textContent = overdueCount;
overdueBadge.classList.remove('hidden');
} else {
overdueBadge.classList.add('hidden');
}
}
// Update unpaid count
const unpaidCount = invoices.filter(inv => !isPaid(inv)).length;
const unpaidBadge = document.getElementById('unpaid-badge');
if (unpaidBadge) {
unpaidBadge.textContent = unpaidCount;
}
}
// ============================================================
// Toolbar HTML
// ============================================================
export function injectToolbar() {
const container = document.getElementById('invoice-toolbar');
if (!container) return;
container.innerHTML = `
<div class="flex flex-wrap items-center gap-3 mb-4 p-4 bg-white rounded-lg shadow-sm border border-gray-200">
<!-- Status Filter Buttons -->
<div class="flex items-center gap-1 border border-gray-300 rounded-lg p-1 bg-gray-100">
<button data-status-filter="all"
onclick="window.invoiceView.setStatus('all')"
class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600">
All
</button>
<button data-status-filter="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">
Unpaid <span id="unpaid-badge" class="ml-1 text-xs opacity-80"></span>
</button>
<button data-status-filter="paid"
onclick="window.invoiceView.setStatus('paid')"
class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600">
Paid
</button>
<button data-status-filter="overdue"
onclick="window.invoiceView.setStatus('overdue')"
class="relative px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600">
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>
</button>
</div>
<div class="w-px h-8 bg-gray-300"></div>
<!-- Customer Filter -->
<div class="flex items-center gap-2">
<label class="text-sm font-medium text-gray-700">Customer:</label>
<input type="text" id="invoice-filter-customer" placeholder="Filter by name..."
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 class="w-px h-8 bg-gray-300"></div>
<!-- Group By -->
<div class="flex items-center gap-2">
<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">
<option value="none">None</option>
<option value="week">Week</option>
<option value="month">Month</option>
</select>
</div>
<!-- Invoice Count -->
<div class="ml-auto text-sm text-gray-500">
<span id="invoice-count" class="font-semibold text-gray-700">0</span> invoices
</div>
</div>
`;
// Event Listeners
document.getElementById('invoice-filter-customer').addEventListener('input', (e) => {
filterCustomer = e.target.value;
renderInvoiceView();
});
document.getElementById('invoice-group-by').addEventListener('change', (e) => {
groupBy = e.target.value;
renderInvoiceView();
});
}
// ============================================================
// Actions
// ============================================================
export function setStatus(status) {
filterStatus = status;
renderInvoiceView();
}
export function viewPDF(id) {
window.open(`/api/invoices/${id}/pdf`, '_blank');
}
export function viewHTML(id) {
window.open(`/api/invoices/${id}/html`, '_blank');
}
export async function exportToQBO(id) {
if (!confirm('Rechnung wirklich an QuickBooks Online senden?')) return;
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = "⏳...";
btn.disabled = true;
try {
const response = await fetch(`/api/invoices/${id}/export`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
alert(`✅ Erfolg! QBO ID: ${result.qbo_id}, Rechnungsnr: ${result.qbo_doc_number}`);
loadInvoices();
} else {
alert(`❌ Fehler: ${result.error}`);
}
} catch (error) {
console.error(error);
alert('Netzwerkfehler beim Export.');
} finally {
btn.textContent = originalText;
btn.disabled = false;
}
}
export async function markPaid(id) {
try {
const response = await fetch(`/api/invoices/${id}/mark-paid`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paid_date: new Date().toISOString().split('T')[0] })
});
if (response.ok) {
loadInvoices();
} else {
const err = await response.json();
alert('Error: ' + (err.error || 'Unknown'));
}
} catch (error) {
console.error('Error marking paid:', error);
}
}
export async function markUnpaid(id) {
try {
const response = await fetch(`/api/invoices/${id}/mark-unpaid`, { method: 'PATCH' });
if (response.ok) {
loadInvoices();
} else {
const err = await response.json();
alert('Error: ' + (err.error || 'Unknown'));
}
} catch (error) {
console.error('Error marking unpaid:', error);
}
}
export async function edit(id) {
if (typeof window.openInvoiceModal === 'function') {
await window.openInvoiceModal(id);
}
}
export async function remove(id) {
if (!confirm('Are you sure you want to delete this invoice?')) return;
try {
const response = await fetch(`/api/invoices/${id}`, { method: 'DELETE' });
if (response.ok) {
loadInvoices();
} else {
alert('Error deleting invoice');
}
} catch (error) {
console.error('Error:', error);
}
}
// ============================================================
// Expose to window
// ============================================================
window.invoiceView = {
viewPDF,
viewHTML,
exportToQBO,
markPaid,
markUnpaid,
edit,
remove,
loadInvoices,
renderInvoiceView,
setStatus
};

View File

@ -1534,6 +1534,55 @@ app.post('/api/qbo/import-unpaid', async (req, res) => {
}
});
// Mark invoice as paid
app.patch('/api/invoices/:id/mark-paid', async (req, res) => {
const { id } = req.params;
const { paid_date } = req.body; // Optional: explizites Datum, sonst heute
try {
const dateToUse = paid_date || new Date().toISOString().split('T')[0];
const result = await pool.query(
'UPDATE invoices SET paid_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
[dateToUse, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Invoice not found' });
}
console.log(`💰 Invoice #${result.rows[0].invoice_number} als bezahlt markiert (${dateToUse})`);
res.json(result.rows[0]);
} catch (error) {
console.error('Error marking invoice as paid:', error);
res.status(500).json({ error: 'Error marking invoice as paid' });
}
});
// Mark invoice as unpaid
app.patch('/api/invoices/:id/mark-unpaid', async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
'UPDATE invoices SET paid_date = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Invoice not found' });
}
console.log(`↩️ Invoice #${result.rows[0].invoice_number} als unbezahlt markiert`);
res.json(result.rows[0]);
} catch (error) {
console.error('Error marking invoice as unpaid:', error);
res.status(500).json({ error: 'Error marking invoice as unpaid' });
}
});
// Start server and browser

View File

@ -86,7 +86,11 @@
font-size: 14px;
line-height: 1.5;
}
.info-div {
display: flex;
height: fit-content;
margin-top: auto;
}
.info-table {
border-collapse: collapse;
font-size: 13px;
@ -110,7 +114,7 @@
.items-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
margin: 40px 0 20px 0;
font-size: 13px;
}
@ -238,6 +242,7 @@
{{CUSTOMER_CITY}}, {{CUSTOMER_STATE}} {{CUSTOMER_ZIP}}
</div>
</div>
<div class="info-div">
<table class="info-table">
<thead>
<tr>
@ -257,6 +262,7 @@
</tbody>
</table>
</div>
</div>
{{AUTHORIZATION}}