523 lines
16 KiB
PHP
523 lines
16 KiB
PHP
<?php
|
|
session_start();
|
|
require '../config.php';
|
|
require '../dbconn.php';
|
|
require '../assets/utils/db_helpers.php';
|
|
require '../assets/utils/classes/InvoiceManager.php';
|
|
|
|
// ── Session guard ─────────────────────────────────────────────────────────
|
|
if (empty($_SESSION['login_company_id'])) {
|
|
header('Location: ' . $server_url . 'login/index.php');
|
|
exit;
|
|
}
|
|
|
|
$company_id = (int)$_SESSION['login_company_id'];
|
|
$invoice_id = (int)($_GET['id'] ?? 0);
|
|
|
|
if (!$invoice_id) {
|
|
http_response_code(400);
|
|
exit('Invalid invoice ID.');
|
|
}
|
|
|
|
// ── Load invoice ──────────────────────────────────────────────────────────
|
|
$inv_mgr = new InvoiceManager($pdo2, $company_id);
|
|
$inv = $inv_mgr->getInvoiceById($invoice_id);
|
|
|
|
if (!$inv) {
|
|
http_response_code(404);
|
|
exit('Invoice not found.');
|
|
}
|
|
|
|
// ── Load company profile ──────────────────────────────────────────────────
|
|
$sth = $pdo1->prepare(
|
|
"SELECT company_name, company_name2, company_logo, address, address2,
|
|
tax_id, phone, email, fax, branch, branch_no
|
|
FROM company_list
|
|
WHERE company_id = :company_id
|
|
LIMIT 1"
|
|
);
|
|
$sth->execute([':company_id' => $company_id]);
|
|
$co = $sth->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
function h(mixed $v): string {
|
|
return htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
function fmt(mixed $v, int $dp = 2): string {
|
|
return number_format((float)$v, $dp);
|
|
}
|
|
|
|
function fmt_date(string $d): string {
|
|
if (!$d) return '—';
|
|
$ts = strtotime($d);
|
|
return $ts ? date('d/m/Y', $ts) : h($d);
|
|
}
|
|
|
|
$items = $inv['items'] ?? [];
|
|
$logo_url = !empty($co['company_logo'])
|
|
? $server_url . 'uploads/company/' . $co['company_logo']
|
|
: '';
|
|
|
|
$doc_labels = [
|
|
'invoice' => 'INVOICE',
|
|
'credit_note' => 'CREDIT NOTE',
|
|
'supplier_credit_note' => 'SUPPLIER CREDIT NOTE',
|
|
];
|
|
$doc_label = $doc_labels[$inv['doc_type'] ?? 'invoice'] ?? 'INVOICE';
|
|
|
|
$status_labels = [
|
|
'0' => 'Draft',
|
|
'1' => 'Issued',
|
|
'2' => 'Paid',
|
|
'3' => 'Overdue',
|
|
'4' => 'Void',
|
|
];
|
|
$status_label = $status_labels[strval($inv['status'] ?? 0)] ?? '';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?= h($doc_label) ?> — <?= h($inv['invoice_number']) ?></title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
font-size: 13px;
|
|
color: #1a1a1a;
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
/* ── Print action bar ── */
|
|
#print_bar {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0;
|
|
background: #1e293b;
|
|
color: #fff;
|
|
padding: 10px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
z-index: 100;
|
|
gap: 12px;
|
|
}
|
|
#print_bar span { font-size: 13px; opacity: 0.75; }
|
|
.btn-print {
|
|
background: #3b82f6;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 20px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.btn-print:hover { background: #2563eb; }
|
|
.btn-back {
|
|
background: transparent;
|
|
color: #94a3b8;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 7px 14px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.btn-back:hover { color: #fff; border-color: #64748b; }
|
|
|
|
/* ── Page wrapper ── */
|
|
.page-wrap {
|
|
padding: 52px 0 32px;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* ── A4 sheet ── */
|
|
.sheet {
|
|
background: #fff;
|
|
width: 794px; /* A4 at 96dpi */
|
|
min-height: 1123px;
|
|
padding: 48px 52px;
|
|
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.inv-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 32px;
|
|
padding-bottom: 24px;
|
|
border-bottom: 2px solid #1e293b;
|
|
}
|
|
.inv-header .co-block img {
|
|
max-height: 56px;
|
|
max-width: 160px;
|
|
object-fit: contain;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
.inv-header .co-block .co-name {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
line-height: 1.3;
|
|
}
|
|
.inv-header .co-block .co-name2 {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
margin-top: 1px;
|
|
}
|
|
.inv-header .co-block .co-detail {
|
|
margin-top: 6px;
|
|
color: #475569;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
.inv-header .doc-block {
|
|
text-align: right;
|
|
}
|
|
.inv-header .doc-block .doc-type {
|
|
font-size: 26px;
|
|
font-weight: 800;
|
|
color: #1e293b;
|
|
letter-spacing: 1px;
|
|
line-height: 1;
|
|
}
|
|
.inv-header .doc-block .doc-number {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #3b82f6;
|
|
margin-top: 6px;
|
|
}
|
|
.inv-header .doc-block .doc-meta {
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
color: #475569;
|
|
line-height: 1.8;
|
|
}
|
|
.inv-header .doc-block .doc-meta strong {
|
|
color: #1e293b;
|
|
font-weight: 600;
|
|
display: inline-block;
|
|
min-width: 70px;
|
|
}
|
|
.status-pill {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
border-radius: 20px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
margin-top: 8px;
|
|
}
|
|
.status-draft { background: #f1f5f9; color: #64748b; }
|
|
.status-issued { background: #dbeafe; color: #1d4ed8; }
|
|
.status-paid { background: #dcfce7; color: #15803d; }
|
|
.status-overdue { background: #fee2e2; color: #b91c1c; }
|
|
.status-void { background: #f1f5f9; color: #94a3b8; }
|
|
|
|
/* ── Bill to ── */
|
|
.bill-section {
|
|
display: flex;
|
|
gap: 32px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.bill-box {
|
|
flex: 1;
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
padding: 14px 18px;
|
|
}
|
|
.bill-box .bill-label {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
color: #94a3b8;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 6px;
|
|
}
|
|
.bill-box .bill-name {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
margin-bottom: 3px;
|
|
}
|
|
.bill-box .bill-detail {
|
|
font-size: 12px;
|
|
color: #475569;
|
|
line-height: 1.6;
|
|
white-space: pre-line;
|
|
}
|
|
|
|
/* ── Items table ── */
|
|
.items-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 0;
|
|
}
|
|
.items-table thead th {
|
|
background: #1e293b;
|
|
color: #fff;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
text-transform: uppercase;
|
|
padding: 10px 12px;
|
|
}
|
|
.items-table thead th:first-child { border-radius: 6px 0 0 0; }
|
|
.items-table thead th:last-child { border-radius: 0 6px 0 0; }
|
|
.items-table thead th.text-end { text-align: right; }
|
|
.items-table tbody tr {
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
.items-table tbody tr:last-child { border-bottom: 2px solid #e2e8f0; }
|
|
.items-table tbody tr:nth-child(even) { background: #f8fafc; }
|
|
.items-table tbody td {
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
color: #334155;
|
|
vertical-align: top;
|
|
}
|
|
.items-table tbody td.text-end { text-align: right; }
|
|
.items-table tbody td .sku {
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ── Totals ── */
|
|
.totals-wrap {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 0;
|
|
}
|
|
.totals-table {
|
|
width: 300px;
|
|
border-collapse: collapse;
|
|
}
|
|
.totals-table td {
|
|
padding: 5px 12px;
|
|
font-size: 13px;
|
|
color: #475569;
|
|
}
|
|
.totals-table td:last-child { text-align: right; }
|
|
.totals-table .total-row td {
|
|
border-top: 2px solid #1e293b;
|
|
padding-top: 10px;
|
|
padding-bottom: 10px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
}
|
|
|
|
/* ── Notes ── */
|
|
.notes-section {
|
|
margin-top: 28px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid #e2e8f0;
|
|
}
|
|
.notes-section .notes-label {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
color: #94a3b8;
|
|
margin-bottom: 6px;
|
|
}
|
|
.notes-section .notes-text {
|
|
font-size: 12px;
|
|
color: #475569;
|
|
line-height: 1.7;
|
|
white-space: pre-line;
|
|
}
|
|
|
|
/* ── Footer ── */
|
|
.inv-footer {
|
|
margin-top: 40px;
|
|
padding-top: 14px;
|
|
border-top: 1px solid #e2e8f0;
|
|
text-align: center;
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
/* ── Print styles ── */
|
|
@media print {
|
|
#print_bar { display: none !important; }
|
|
body { background: #fff; }
|
|
.page-wrap { padding: 0; }
|
|
.sheet { width: 100%; min-height: auto; padding: 16mm 18mm; box-shadow: none; }
|
|
@page { size: A4; margin: 0; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Print bar (hidden on print) -->
|
|
<div id="print_bar">
|
|
<a href="javascript:history.back()" class="btn-back">← Back</a>
|
|
<span><?= h($doc_label) ?> / <?= h($inv['invoice_number']) ?></span>
|
|
<button class="btn-print" onclick="window.print()">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
|
Print / Save PDF
|
|
</button>
|
|
</div>
|
|
|
|
<div class="page-wrap">
|
|
<div class="sheet">
|
|
|
|
<!-- Header: company left, doc info right -->
|
|
<div class="inv-header">
|
|
<div class="co-block">
|
|
<?php if ($logo_url): ?>
|
|
<img src="<?= h($logo_url) ?>" alt="Logo">
|
|
<?php endif; ?>
|
|
<div class="co-name"><?= h($co['company_name'] ?? '') ?></div>
|
|
<?php if (!empty($co['company_name2'])): ?>
|
|
<div class="co-name2"><?= h($co['company_name2']) ?></div>
|
|
<?php endif; ?>
|
|
<div class="co-detail">
|
|
<?php
|
|
$co_lines = array_filter([
|
|
$co['address'] ?? '',
|
|
$co['address2'] ?? '',
|
|
!empty($co['tax_id']) ? 'Tax ID: ' . $co['tax_id'] : '',
|
|
!empty($co['phone']) ? 'Tel: ' . $co['phone'] : '',
|
|
!empty($co['email']) ? $co['email'] : '',
|
|
]);
|
|
echo implode('<br>', array_map('h', $co_lines));
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="doc-block">
|
|
<div class="doc-type"><?= h($doc_label) ?></div>
|
|
<div class="doc-number"><?= h($inv['invoice_number']) ?></div>
|
|
<div class="doc-meta">
|
|
<div><strong>Issued</strong> <?= h(fmt_date($inv['issued_date'] ?? '')) ?></div>
|
|
<?php if (!empty($inv['due_date'])): ?>
|
|
<div><strong>Due</strong> <?= h(fmt_date($inv['due_date'])) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($inv['order_number'])): ?>
|
|
<div><strong>Order</strong> <?= h($inv['order_number']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
$status_classes = [
|
|
'0' => 'status-draft',
|
|
'1' => 'status-issued',
|
|
'2' => 'status-paid',
|
|
'3' => 'status-overdue',
|
|
'4' => 'status-void',
|
|
];
|
|
$sc = $status_classes[strval($inv['status'] ?? 0)] ?? 'status-draft';
|
|
?>
|
|
<div class="status-pill <?= $sc ?>"><?= h($status_label) ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bill To -->
|
|
<div class="bill-section">
|
|
<div class="bill-box">
|
|
<div class="bill-label">Bill To</div>
|
|
<div class="bill-name"><?= h($inv['contact_name'] ?? '—') ?></div>
|
|
<?php if (!empty($inv['contact_billing_address'])): ?>
|
|
<div class="bill-detail"><?= h($inv['contact_billing_address']) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($inv['contact_tax_id'])): ?>
|
|
<div class="bill-detail" style="margin-top:4px;">Tax ID: <?= h($inv['contact_tax_id']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Items table -->
|
|
<table class="items-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40px;">#</th>
|
|
<th>Product</th>
|
|
<th class="text-end" style="width:80px;">Qty</th>
|
|
<th class="text-end" style="width:110px;">Unit Price</th>
|
|
<th class="text-end" style="width:110px;">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($items as $i => $item): ?>
|
|
<tr>
|
|
<td style="color:#94a3b8;"><?= $i + 1 ?></td>
|
|
<td>
|
|
<?= h($item['product_name'] ?? $item['product_sku'] ?? '') ?>
|
|
<?php if (!empty($item['product_sku']) && !empty($item['product_name'])): ?>
|
|
<div class="sku"><?= h($item['product_sku']) ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="text-end"><?= h(fmt($item['quantity'] ?? 0)) ?></td>
|
|
<td class="text-end"><?= h(fmt($item['price'] ?? 0)) ?></td>
|
|
<td class="text-end"><?= h(fmt($item['total_price'] ?? 0)) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($items)): ?>
|
|
<tr><td colspan="5" style="text-align:center;color:#94a3b8;padding:20px;">No items</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Totals -->
|
|
<div class="totals-wrap">
|
|
<table class="totals-table">
|
|
<?php
|
|
$rows = [
|
|
['Subtotal', $inv['subtotal'] ?? 0],
|
|
['Discount', $inv['discount'] ?? 0],
|
|
['Tax', $inv['tax'] ?? 0],
|
|
['Shipping', $inv['shipping_fee'] ?? 0],
|
|
];
|
|
foreach ($rows as [$label, $val]):
|
|
?>
|
|
<tr>
|
|
<td><?= h($label) ?></td>
|
|
<td><?= h(fmt($val)) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<tr class="total-row">
|
|
<td>Grand Total</td>
|
|
<td><?= h(fmt($inv['grand_total'] ?? 0)) ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<?php if (!empty($inv['notes'])): ?>
|
|
<div class="notes-section">
|
|
<div class="notes-label">Notes</div>
|
|
<div class="notes-text"><?= h($inv['notes']) ?></div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Footer -->
|
|
<div class="inv-footer">
|
|
<?php
|
|
$footer_parts = array_filter([
|
|
$co['company_name'] ?? '',
|
|
!empty($co['phone']) ? 'Tel: ' . $co['phone'] : '',
|
|
$co['email'] ?? '',
|
|
$co['website'] ?? '',
|
|
]);
|
|
echo implode(' · ', array_map('h', $footer_parts));
|
|
?>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|