Files
wms-app/app/order/print_invoice.php
T

523 lines
16 KiB
PHP

<?php
require_once __DIR__ . '/../session.php';
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) ?> &nbsp;/&nbsp; <?= 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(' &nbsp;·&nbsp; ', array_map('h', $footer_parts));
?>
</div>
</div>
</div>
</body>
</html>