feat(medical-store): add server-side A4/A5 vector PDF generation with mPDF and 48-col plain-text thermal receipt printing
This commit is contained in:
parent
c8a2d50df9
commit
82cae89fae
|
|
@ -1671,6 +1671,7 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
|
|||
$routes->get('medical-store/stock/alerts', 'MedicalStoreApi::stockAlerts');
|
||||
$routes->post('medical-store/sales/save', 'MedicalStoreApi::saveSale');
|
||||
$routes->get('medical-store/sales/invoice/(:num)', 'MedicalStoreApi::getInvoice/$1');
|
||||
$routes->get('medical-store/sales/invoice-pdf/(:num)', 'MedicalStoreApi::invoicePdf/$1');
|
||||
$routes->get('medical-store/sales/recent', 'MedicalStoreApi::recentSales');
|
||||
$routes->get('medical-store/suppliers', 'MedicalStoreApi::suppliers');
|
||||
$routes->post('medical-store/suppliers/save', 'MedicalStoreApi::saveSupplier');
|
||||
|
|
|
|||
|
|
@ -1901,7 +1901,7 @@ class MedicalStoreApi extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function getInvoice(int $saleId)
|
||||
protected function getInvoiceData(int $saleId): ?array
|
||||
{
|
||||
$sale = $this->db->table('mst_sales s')
|
||||
->select('s.*, st.store_name, st.building_name, st.floor_no, st.room_no, st.drug_license_no_20b, st.drug_license_no_21b, st.drug_license_no_20f_x, st.gstin, st.pan_no, st.fssai_no, st.state_code, st.state_name, st.registered_pharmacist_name, st.pharmacist_reg_no, st.contact_phone, st.contact_email, st.address as store_address, st.upi_id, st.terms_conditions, st.abdm_hfr_id, st.abdm_hip_id, st.pharmacist_hpr_id')
|
||||
|
|
@ -1911,7 +1911,7 @@ class MedicalStoreApi extends BaseController
|
|||
->getRowArray();
|
||||
|
||||
if (!$sale) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Invoice not found.']);
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = $this->db->table('mst_sales_items si')
|
||||
|
|
@ -1967,8 +1967,7 @@ class MedicalStoreApi extends BaseController
|
|||
$upiQrString = "upi://pay?pa={$sale['upi_id']}&pn=" . urlencode($sale['store_name']) . "&am={$sale['net_amount']}&cu=INR&tn=" . urlencode("Bill " . $sale['invoice_no']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
return [
|
||||
'sale' => $sale,
|
||||
'items' => $saleItems,
|
||||
'return_items' => $returnItems,
|
||||
|
|
@ -1976,9 +1975,540 @@ class MedicalStoreApi extends BaseController
|
|||
'credit_note' => $creditNote,
|
||||
'hsn_summary' => array_values($hsnSummary),
|
||||
'upi_qr_string' => $upiQrString
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
public function getInvoice(int $saleId)
|
||||
{
|
||||
$data = $this->getInvoiceData($saleId);
|
||||
if (!$data) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Invoice not found.']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON(array_merge(['status' => 1], $data));
|
||||
}
|
||||
|
||||
public function invoicePdf(int $saleId)
|
||||
{
|
||||
$data = $this->getInvoiceData($saleId);
|
||||
if (!$data) {
|
||||
return $this->response->setStatusCode(404)->setBody('Invoice not found.');
|
||||
}
|
||||
|
||||
$format = strtolower($this->request->getGet('format') ?? 'a4');
|
||||
$download = (bool)($this->request->getGet('download') ?? false);
|
||||
$isA5 = ($format === 'a5');
|
||||
|
||||
$tmpDir = WRITEPATH . 'cache/mpdf';
|
||||
if (!is_dir($tmpDir)) {
|
||||
@mkdir($tmpDir, 0777, true);
|
||||
}
|
||||
|
||||
$mpdfConfig = [
|
||||
'tempDir' => $tmpDir,
|
||||
'mode' => 'utf-8',
|
||||
'format' => $isA5 ? 'A5' : 'A4',
|
||||
'orientation' => 'P',
|
||||
'margin_left' => $isA5 ? 6 : 8,
|
||||
'margin_right' => $isA5 ? 6 : 8,
|
||||
'margin_top' => $isA5 ? 6 : 8,
|
||||
'margin_bottom' => $isA5 ? 6 : 8,
|
||||
'default_font' => 'dejavusans'
|
||||
];
|
||||
|
||||
try {
|
||||
$mpdf = new \Mpdf\Mpdf($mpdfConfig);
|
||||
$html = $this->renderInvoicePdfHtml($data, $isA5);
|
||||
$mpdf->WriteHTML($html);
|
||||
|
||||
$safeInv = preg_replace('/[^A-Za-z0-9_-]/', '_', $data['sale']['invoice_no'] ?: ('INV_' . $saleId));
|
||||
$fileName = "Invoice_{$safeInv}_{$format}.pdf";
|
||||
|
||||
$pdfBinary = $mpdf->Output('', \Mpdf\Output\Destination::STRING_RETURN);
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/pdf')
|
||||
->setHeader('Content-Disposition', ($download ? 'attachment' : 'inline') . '; filename="' . $fileName . '"')
|
||||
->setBody($pdfBinary);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'MedicalStoreApi::invoicePdf failed: ' . $e->getMessage());
|
||||
return $this->response->setStatusCode(500)->setBody('Failed to generate PDF: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function renderInvoicePdfHtml(array $data, bool $isA5): string
|
||||
{
|
||||
$sale = $data['sale'];
|
||||
$items = $data['items'];
|
||||
$returnItems = $data['return_items'];
|
||||
$creditNote = $data['credit_note'];
|
||||
$hsnSummary = $data['hsn_summary'];
|
||||
$upiQrString = $data['upi_qr_string'];
|
||||
|
||||
$esc = fn($v) => htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
$curr = fn($v) => '₹' . number_format((float)($v ?? 0), 2, '.', '');
|
||||
|
||||
$fontSize = $isA5 ? '7pt' : '8pt';
|
||||
$thPadding = $isA5 ? '2.5px 3.5px' : '4px 5px';
|
||||
$tdPadding = $isA5 ? '2.5px 3.5px' : '3.5px 5px';
|
||||
|
||||
$qrCodeValue = $upiQrString ?: $sale['invoice_no'];
|
||||
$qrHtml = '';
|
||||
if (!empty($qrCodeValue)) {
|
||||
$qrSize = $isA5 ? '0.55' : '0.65';
|
||||
$qrHtml = '<barcode code="' . $esc($qrCodeValue) . '" size="' . $qrSize . '" type="QR" error="M" class="barcode" />';
|
||||
}
|
||||
|
||||
$html = '
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: dejavusans, sans-serif;
|
||||
font-size: ' . $fontSize . ';
|
||||
color: #0f172a;
|
||||
line-height: 1.25;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.header-table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.border-box {
|
||||
border: 0.3mm solid #94a3b8;
|
||||
border-radius: 1.5mm;
|
||||
padding: ' . ($isA5 ? '4px' : '6px') . ';
|
||||
background-color: #f8fafc;
|
||||
margin-bottom: ' . ($isA5 ? '4px' : '6px') . ';
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: ' . ($isA5 ? '4px' : '6px') . ';
|
||||
}
|
||||
.items-table th {
|
||||
background-color: #0f766e;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';
|
||||
padding: ' . $thPadding . ';
|
||||
border: 0.2mm solid #0f766e;
|
||||
text-align: left;
|
||||
}
|
||||
.items-table td {
|
||||
padding: ' . $tdPadding . ';
|
||||
border: 0.2mm solid #cbd5e1;
|
||||
font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';
|
||||
vertical-align: top;
|
||||
}
|
||||
.items-table tr:nth-child(even) td {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.return-table th {
|
||||
background-color: #be123c !important;
|
||||
border-color: #be123c !important;
|
||||
}
|
||||
.return-table td {
|
||||
border-color: #fecdd3 !important;
|
||||
background-color: #fff1f2 !important;
|
||||
color: #9f1239;
|
||||
}
|
||||
.hsn-table th {
|
||||
background-color: #334155;
|
||||
color: #ffffff;
|
||||
font-size: ' . ($isA5 ? '6pt' : '7pt') . ';
|
||||
padding: ' . ($isA5 ? '2px 3px' : '3px 4px') . ';
|
||||
border: 0.2mm solid #334155;
|
||||
}
|
||||
.hsn-table td {
|
||||
font-size: ' . ($isA5 ? '6pt' : '7pt') . ';
|
||||
padding: ' . ($isA5 ? '2px 3px' : '3px 4px') . ';
|
||||
border: 0.2mm solid #cbd5e1;
|
||||
text-align: center;
|
||||
}
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
.fw-bold { font-weight: bold; }
|
||||
.text-muted { color: #64748b; }
|
||||
.text-primary { color: #0284c7; }
|
||||
.text-danger { color: #dc2626; }
|
||||
.text-success { color: #16a34a; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 1px 4px;
|
||||
border-radius: 1mm;
|
||||
font-size: 6pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
.badge-teal { background-color: #ccfbf1; color: #0f766e; border: 0.2mm solid #14b8a6; }
|
||||
.badge-green { background-color: #dcfce7; color: #15803d; border: 0.2mm solid #22c55e; }
|
||||
.badge-blue { background-color: #e0f2fe; color: #0369a1; border: 0.2mm solid #0ea5e9; }
|
||||
.total-box {
|
||||
background-color: #f0fdf4;
|
||||
border: 0.4mm solid #16a34a;
|
||||
padding: ' . ($isA5 ? '3px 5px' : '5px 7px') . ';
|
||||
border-radius: 1.5mm;
|
||||
color: #14532d;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>';
|
||||
|
||||
// 1. Header Table
|
||||
$html .= '
|
||||
<table class="header-table" style="margin-bottom: ' . ($isA5 ? '4px' : '8px') . ';">
|
||||
<tr>
|
||||
<td style="width: 66%;">
|
||||
<div style="font-size: ' . ($isA5 ? '11pt' : '13pt') . '; font-weight: bold; color: #0f766e; margin-bottom: 2px;">
|
||||
' . $esc($sale['store_name'] ?: 'City Hospital Central Pharmacy') . '
|
||||
</div>
|
||||
<div style="color: #475569; font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';">
|
||||
' . $esc($sale['building_name'] ?: 'Main Hospital Block A') . ', ' . $esc($sale['floor_no'] ?: 'Ground Floor') . ($sale['store_address'] ? ', ' . $esc($sale['store_address']) : '') . '
|
||||
</div>
|
||||
<div style="font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . '; margin-top: 2px;">
|
||||
<strong>DL 20-B:</strong> ' . $esc($sale['drug_license_no_20b'] ?: 'N/A') . ' | <strong>DL 21-B:</strong> ' . $esc($sale['drug_license_no_21b'] ?: 'N/A') . '
|
||||
' . ($sale['fssai_no'] ? ' | <strong>FSSAI:</strong> ' . $esc($sale['fssai_no']) : '') . '
|
||||
</div>
|
||||
<div style="font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';">
|
||||
<strong>GSTIN:</strong> ' . $esc($sale['gstin'] ?: 'N/A') . ' | <strong>State:</strong> ' . $esc($sale['state_name'] ?: 'Delhi') . ' (Code: ' . $esc($sale['state_code'] ?: '07') . ')
|
||||
' . ($sale['abdm_hfr_id'] ? ' | <strong>ABDM HFR:</strong> ' . $esc($sale['abdm_hfr_id']) : '') . '
|
||||
</div>
|
||||
</td>
|
||||
<td style="width: 34%; text-align: right;">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td style="text-align: right; vertical-align: top;">
|
||||
<div style="display: inline-block; background-color: #0f766e; color: #ffffff; padding: 2px 6px; font-weight: bold; font-size: ' . ($isA5 ? '7pt' : '8pt') . '; border-radius: 1mm; margin-bottom: 2px;">
|
||||
' . ($isA5 ? 'RETAIL TAX INVOICE' : 'HOSPITAL GST TAX INVOICE') . '
|
||||
</div>
|
||||
<div style="font-size: ' . ($isA5 ? '9pt' : '11pt') . '; font-weight: bold; color: #0f172a;">
|
||||
' . $esc($sale['invoice_no']) . '
|
||||
</div>
|
||||
<div style="color: #64748b; font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';">
|
||||
' . $esc($sale['sale_date']) . '
|
||||
</div>
|
||||
</td>
|
||||
' . ($qrHtml ? '<td style="width: 45px; text-align: right; vertical-align: top; padding-left: 5px;">' . $qrHtml . '</td>' : '') . '
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>';
|
||||
|
||||
// 2. Patient & Doctor Info Table
|
||||
$html .= '
|
||||
<div class="border-box">
|
||||
<table style="font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';">
|
||||
<tr>
|
||||
<td style="width: 35%;">
|
||||
<strong>Patient Name:</strong> ' . $esc($sale['patient_name'] ?: 'Walk-in Customer') . '<br>
|
||||
<strong>UHID No:</strong> <span class="text-primary fw-bold">' . $esc($sale['uhid'] ?: 'Walk-in') . '</span>
|
||||
</td>
|
||||
<td style="width: 30%;">
|
||||
<strong>Phone:</strong> ' . $esc($sale['patient_mobile'] ?: 'N/A') . '<br>
|
||||
<strong>Age / Gender:</strong> ' . $esc($sale['age'] ?: 'N/A') . ' / ' . $esc($sale['gender'] ?: 'N/A') . '
|
||||
</td>
|
||||
<td style="width: 35%;">
|
||||
<strong>Prescribing Doctor:</strong> ' . $esc($sale['doctor_name'] ?: 'Dr. Consultant') . '<br>
|
||||
<strong>Doctor Reg No:</strong> ' . $esc($sale['doctor_reg_no'] ?: 'N/A') . '
|
||||
</td>
|
||||
</tr>';
|
||||
|
||||
if (!empty($sale['abha_id']) || !empty($sale['abha_address']) || !empty($sale['abdm_care_context_ref'])) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td colspan="3" style="padding-top: 3px; border-top: 0.2mm solid #e2e8f0; color: #15803d; font-weight: bold;">
|
||||
✓ ABDM Linked: ABHA No: ' . $esc($sale['abha_id'] ?: 'N/A') . ' | Address: ' . $esc($sale['abha_address'] ?: 'N/A') . ($sale['abdm_care_context_ref'] ? ' | Care Context: ' . $esc($sale['abdm_care_context_ref']) : '') . '
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
if (!empty($sale['ipd_id'])) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td colspan="3" style="padding-top: 2px; border-top: 0.2mm solid #e2e8f0; color: #b91c1c; font-weight: bold;">
|
||||
🏥 IPD Admission #' . $esc($sale['ipd_id']) . ' • Ward: ' . $esc($sale['ward_name'] ?: 'General') . ' • Bed: ' . $esc($sale['bed_no'] ?: 'Bed') . ' (Charged to IPD Running Bill)
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
</table>
|
||||
</div>';
|
||||
|
||||
// 3. Dispensed Medicines Table
|
||||
$html .= '
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 3%; text-align: center;">#</th>
|
||||
<th style="width: 27%;">Medicine & Formulation</th>
|
||||
<th style="width: 8%; text-align: center;">HSN</th>
|
||||
<th style="width: 10%; text-align: center;">Batch</th>
|
||||
<th style="width: 8%; text-align: center;">Exp</th>
|
||||
<th style="width: 10%; text-align: center;">Qty & Unit</th>
|
||||
<th style="width: 8%; text-align: right;">Rate</th>
|
||||
<th style="width: 8%; text-align: right;">Taxable</th>
|
||||
<th style="width: 5%; text-align: right;">CGST</th>
|
||||
<th style="width: 5%; text-align: right;">SGST</th>
|
||||
<th style="width: 8%; text-align: right;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
if (empty($items)) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td colspan="11" class="text-center text-muted" style="padding: 10px;">
|
||||
- No new medicines dispensed (Sales Return & Customer Refund Bill) -
|
||||
</td>
|
||||
</tr>';
|
||||
} else {
|
||||
foreach ($items as $idx => $it) {
|
||||
$qtyUnitStr = '';
|
||||
if ($it['sell_unit'] === 'Tablet') {
|
||||
$qtyUnitStr = $it['total_units'] . ' Tab';
|
||||
} elseif ($it['sell_unit'] === 'Combo') {
|
||||
$qtyUnitStr = $it['qty'] . ' Str + ' . $it['loose_qty'] . ' Tab';
|
||||
} else {
|
||||
$qtyUnitStr = ((int)$it['units_per_pack'] > 1) ? ($it['qty'] . ' Strip') : ($it['qty'] . ' ' . ($it['unit_pack'] ?: 'Unit'));
|
||||
}
|
||||
|
||||
$rateStr = ($it['sell_unit'] === 'Tablet') ? ($curr($it['unit_price']) . '<span style="font-size:5.5pt;color:#64748b;">/tab</span>') : $curr($it['unit_mrp']);
|
||||
|
||||
$html .= '
|
||||
<tr>
|
||||
<td style="text-align: center;">' . ($idx + 1) . '</td>
|
||||
<td>
|
||||
<strong style="color: #0f172a;">' . $esc($it['item_name']) . '</strong>';
|
||||
if ((int)$it['units_per_pack'] > 1 && $it['sell_unit'] !== 'Tablet') {
|
||||
$html .= '<div style="font-size: 6pt; color: #64748b;">Pack: 1 Strip of ' . $esc($it['units_per_pack']) . ' Tabs</div>';
|
||||
}
|
||||
if (!empty($it['snomed_ct_code'])) {
|
||||
$html .= '<div style="font-size: 5.5pt; color: #0284c7;">SNOMED: ' . $esc($it['snomed_ct_code']) . '</div>';
|
||||
}
|
||||
$html .= '
|
||||
</td>
|
||||
<td style="text-align: center;">' . $esc($it['hsn_code'] ?: '3004') . '</td>
|
||||
<td style="text-align: center; font-weight: bold;">' . $esc($it['batch_no']) . '</td>
|
||||
<td style="text-align: center;">' . $esc(substr($it['expiry_date'] ?? '', 0, 7)) . '</td>
|
||||
<td style="text-align: center; font-weight: bold;">' . $esc($qtyUnitStr) . '</td>
|
||||
<td style="text-align: right;">' . $rateStr . '</td>
|
||||
<td style="text-align: right;">' . $curr($it['taxable_value']) . '</td>
|
||||
<td style="text-align: right;">' . $curr($it['cgst_amount']) . '</td>
|
||||
<td style="text-align: right;">' . $curr($it['sgst_amount']) . '</td>
|
||||
<td style="text-align: right; font-weight: bold;">' . $curr($it['total_amount']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
$html .= '
|
||||
</tbody>
|
||||
</table>';
|
||||
|
||||
// 4. Returned / Exchanged Medicines Table (if any)
|
||||
if (!empty($returnItems)) {
|
||||
$html .= '
|
||||
<div style="background-color: #ffe4e6; border: 0.2mm solid #e11d48; padding: 3px 5px; font-weight: bold; color: #9f1239; font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . ';">
|
||||
RETURNED / EXCHANGED MEDICINES ' . ($creditNote ? '(Credit Note: ' . $esc($creditNote['credit_note_no']) . ')' : '') . '
|
||||
</div>
|
||||
<table class="items-table return-table" style="margin-top: -0.2mm;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 3%; text-align: center;">#</th>
|
||||
<th style="width: 27%;">Returned Medicine</th>
|
||||
<th style="width: 8%; text-align: center;">HSN</th>
|
||||
<th style="width: 10%; text-align: center;">Batch</th>
|
||||
<th style="width: 10%; text-align: center;">Original Bill</th>
|
||||
<th style="width: 8%; text-align: center;">Condition</th>
|
||||
<th style="width: 10%; text-align: center;">Qty Returned</th>
|
||||
<th style="width: 8%; text-align: right;">Rate</th>
|
||||
<th style="width: 8%; text-align: right;">Taxable</th>
|
||||
<th style="width: 8%; text-align: right; color: #9f1239;">Credit Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
foreach ($returnItems as $rIdx => $rit) {
|
||||
$retQtyStr = '-' . (($rit['sell_unit'] === 'Tablet') ? ($rit['total_units'] . ' Tab') : ($rit['qty'] . ' ' . ($rit['unit_pack'] ?: 'Unit')));
|
||||
$html .= '
|
||||
<tr>
|
||||
<td style="text-align: center;">' . ($rIdx + 1) . '</td>
|
||||
<td><strike>' . $esc($rit['item_name']) . '</strike></td>
|
||||
<td style="text-align: center;">' . $esc($rit['hsn_code'] ?: '3004') . '</td>
|
||||
<td style="text-align: center;">' . $esc($rit['batch_no']) . '</td>
|
||||
<td style="text-align: center;">' . $esc($rit['ref_invoice_no'] ?: 'N/A') . '</td>
|
||||
<td style="text-align: center;">' . $esc($rit['return_condition'] ?: 'GOOD') . '</td>
|
||||
<td style="text-align: center; font-weight: bold; color: #b91c1c;">' . $esc($retQtyStr) . '</td>
|
||||
<td style="text-align: right;">' . $curr($rit['unit_price']) . '</td>
|
||||
<td style="text-align: right;">-' . $curr(abs((float)$rit['taxable_value'])) . '</td>
|
||||
<td style="text-align: right; font-weight: bold; color: #b91c1c;">-' . $curr(abs((float)$rit['total_amount'])) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
</tbody>
|
||||
</table>';
|
||||
}
|
||||
|
||||
// 5. GST Tax Breakdown Table (HSN Summary)
|
||||
if (!empty($hsnSummary)) {
|
||||
$html .= '
|
||||
<div style="font-size: ' . ($isA5 ? '6pt' : '7pt') . '; font-weight: bold; color: #475569; margin-bottom: 2px;">
|
||||
GST Tax Breakdown (Indian GST Standard):
|
||||
</div>
|
||||
<table class="hsn-table" style="margin-bottom: ' . ($isA5 ? '4px' : '6px') . ';">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 16%;">HSN Code</th>
|
||||
<th style="width: 16%;">GST Rate</th>
|
||||
<th style="width: 20%;">Taxable Value</th>
|
||||
<th style="width: 16%;">CGST</th>
|
||||
<th style="width: 16%;">SGST</th>
|
||||
<th style="width: 16%;">Total Tax</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
foreach ($hsnSummary as $h) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td>' . $esc($h['hsn_code']) . '</td>
|
||||
<td>' . $esc($h['gst_rate']) . '%</td>
|
||||
<td style="text-align: right;">' . $curr($h['taxable_value']) . '</td>
|
||||
<td style="text-align: right;">' . $curr($h['cgst_amount']) . '</td>
|
||||
<td style="text-align: right;">' . $curr($h['sgst_amount']) . '</td>
|
||||
<td style="text-align: right; font-weight: bold;">' . $curr($h['total_tax']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
</tbody>
|
||||
</table>';
|
||||
}
|
||||
|
||||
// 6. Summary Totals & Signatures
|
||||
$html .= '
|
||||
<table style="width: 100%; border-top: 0.3mm solid #94a3b8; padding-top: ' . ($isA5 ? '3px' : '5px') . ';">
|
||||
<tr>
|
||||
<td style="width: 58%; vertical-align: top; padding-right: 8px;">
|
||||
<div style="font-size: ' . ($isA5 ? '6pt' : '7pt') . '; color: #334155; margin-bottom: 3px;">
|
||||
<strong>Registered Pharmacist:</strong> ' . $esc($sale['registered_pharmacist_name'] ?: 'Pharmacist') . ' (Reg No: ' . $esc($sale['pharmacist_reg_no'] ?: 'N/A') . ')
|
||||
' . ($sale['pharmacist_hpr_id'] ? '<br><strong>HPR ID:</strong> ' . $esc($sale['pharmacist_hpr_id']) : '') . '
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8fafc; border: 0.2mm solid #cbd5e1; border-radius: 1mm; padding: 4px; font-size: ' . ($isA5 ? '6pt' : '7pt') . '; margin-bottom: 4px;">
|
||||
<strong>Payment Settlement:</strong> ' . $esc($sale['payment_mode']) . '<br>';
|
||||
|
||||
if ((float)$sale['cash_paid'] > 0) {
|
||||
$html .= '• Cash: ' . $curr($sale['cash_paid']) . ' ';
|
||||
}
|
||||
if ((float)$sale['upi_paid'] > 0) {
|
||||
$html .= '• UPI/Bank: ' . $curr($sale['upi_paid']) . ($sale['upi_ref_no'] ? ' [UTR: ' . $esc($sale['upi_ref_no']) . ']' : '') . ($sale['bank_name'] ? ' (' . $esc($sale['bank_name']) . ')' : '') . ' ';
|
||||
}
|
||||
if ((float)$sale['card_paid'] > 0) {
|
||||
$html .= '• Card: ' . $curr($sale['card_paid']) . ($sale['card_ref_no'] ? ' [Ref: ' . $esc($sale['card_ref_no']) . ']' : '') . ' ';
|
||||
}
|
||||
if ((float)$sale['credit_amount'] > 0) {
|
||||
$html .= '• IPD Credit: ' . $curr($sale['credit_amount']) . ' ';
|
||||
}
|
||||
|
||||
$html .= '<br>' . ($sale['is_bank_reconciled'] == 1 ? '<span style="color:#16a34a;font-weight:bold;">✓ Bank Statement Audited</span>' : '<span style="color:#d97706;">Bank Statement: Pending Audit</span>');
|
||||
|
||||
$html .= '
|
||||
</div>
|
||||
|
||||
<div style="font-size: 5.5pt; color: #64748b; line-height: 1.2;">
|
||||
' . nl2br($esc($sale['terms_conditions'] ?: "1. Goods once sold are returnable only as per Drug Rules within 7 days.\n2. Keep medicines stored in cool & dry place below 25°C away from direct sunlight.")) . '
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="width: 42%; vertical-align: top;">
|
||||
<table style="font-size: ' . ($isA5 ? '6.5pt' : '7.5pt') . '; line-height: 1.35;">
|
||||
<tr>
|
||||
<td>Gross Items Total:</td>
|
||||
<td class="text-right">' . $curr($sale['gross_amount']) . '</td>
|
||||
</tr>';
|
||||
|
||||
if ((float)$sale['discount_amount'] > 0) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td class="text-danger">Total Discount Allowed:</td>
|
||||
<td class="text-right text-danger">-' . $curr($sale['discount_amount']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
if ((float)$sale['return_amount'] > 0) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td class="text-danger fw-bold">Less Return Credit:</td>
|
||||
<td class="text-right text-danger fw-bold">-' . $curr($sale['return_amount']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
<tr>
|
||||
<td>Taxable Amount:</td>
|
||||
<td class="text-right">' . $curr($sale['taxable_amount']) . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CGST Amount:</td>
|
||||
<td class="text-right">' . $curr($sale['cgst_amount']) . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SGST Amount:</td>
|
||||
<td class="text-right">' . $curr($sale['sgst_amount']) . '</td>
|
||||
</tr>';
|
||||
|
||||
if ((float)$sale['round_off'] != 0) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td>Round Off:</td>
|
||||
<td class="text-right">' . $curr($sale['round_off']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
<tr>
|
||||
<td colspan="2" style="padding-top: 3px;">
|
||||
<div class="total-box">
|
||||
<table style="width: 100%; font-size: ' . ($isA5 ? '8pt' : '10pt') . '; font-weight: bold;">
|
||||
<tr>
|
||||
<td>NET PAYABLE:</td>
|
||||
<td style="text-align: right;">' . $curr($sale['net_amount']) . '</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>';
|
||||
|
||||
if ((float)$sale['refund_amount'] > 0) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td colspan="2" style="padding-top: 3px;">
|
||||
<div style="background-color: #ffe4e6; border: 0.3mm solid #e11d48; padding: 3px 5px; border-radius: 1mm; color: #9f1239; font-weight: bold; font-size: ' . ($isA5 ? '7pt' : '8pt') . ';">
|
||||
REFUND PAID TO CUSTOMER: ' . $curr($sale['refund_amount']) . ' (' . $esc($sale['refund_mode']) . ')
|
||||
</div>
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
public function recentSales()
|
||||
{
|
||||
$storeId = (int)($this->request->getGet('store_id') ?? 1);
|
||||
|
|
|
|||
|
|
@ -339,6 +339,10 @@
|
|||
PRINT FORMATS: 80mm Thermal, A5 Slip, and A4 Hospital Tax Invoice
|
||||
------------------------------------------------------------- */
|
||||
@media print {
|
||||
@page {
|
||||
size: auto;
|
||||
margin: 6mm;
|
||||
}
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
@ -349,9 +353,23 @@
|
|||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.print-a4-invoice, .print-a5-invoice {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-height: auto !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.print-thermal-80mm {
|
||||
width: 76mm !important;
|
||||
max-width: 100% !important;
|
||||
border: none !important;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
|
|
@ -366,39 +384,42 @@
|
|||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
color: #000;
|
||||
padding: 6px;
|
||||
padding: 8px;
|
||||
margin: auto;
|
||||
background: #fff;
|
||||
border: 1px dashed #ccc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 2. A5 Half-Page Invoice */
|
||||
.print-a5-invoice {
|
||||
width: 148mm;
|
||||
min-height: 210mm;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
min-height: auto;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 11px;
|
||||
color: #1e293b;
|
||||
padding: 14px;
|
||||
padding: 16px;
|
||||
margin: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* 3. A4 Full Comprehensive Tax Invoice */
|
||||
.print-a4-invoice {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: 820px;
|
||||
min-height: auto;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 11.5px;
|
||||
color: #0f172a;
|
||||
padding: 24px;
|
||||
padding: 20px;
|
||||
margin: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
|
|
@ -877,6 +898,24 @@ function PosCounter({ store, notify }) {
|
|||
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
|
||||
const [isPrintOpen, setIsPrintOpen] = useState(false);
|
||||
|
||||
// Automatically preload latest completed sale for this store so "Last Bill" is immediately available
|
||||
useEffect(() => {
|
||||
if (store && store.store_id && !lastInvoice) {
|
||||
fetch(API_BASE + `sales/recent?store_id=${store.store_id}&limit=1`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.status && d.sales && d.sales.length > 0) {
|
||||
const latestSaleId = d.sales[0].sale_id;
|
||||
fetch(API_BASE + `sales/invoice/${latestSaleId}`)
|
||||
.then(ir => ir.json())
|
||||
.then(id => {
|
||||
if (id.status) setLastInvoice(id);
|
||||
}).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, [store?.store_id]);
|
||||
|
||||
// B2C Sales Return & Same-Invoice Exchange State
|
||||
const [returnCart, setReturnCart] = useState([]);
|
||||
const [showReturnModal, setShowReturnModal] = useState(false);
|
||||
|
|
@ -1664,6 +1703,259 @@ function PosCounter({ store, notify }) {
|
|||
}
|
||||
};
|
||||
|
||||
const generateThermalReceiptText = (inv) => {
|
||||
if (!inv || !inv.sale) return '';
|
||||
const sale = inv.sale;
|
||||
const items = inv.items || [];
|
||||
const returnItems = inv.return_items || [];
|
||||
const LINE_WIDTH = 48; // Standard 80mm thermal receipt line length (Font A: 48 chars)
|
||||
|
||||
const center = (str) => {
|
||||
str = (str || '').trim();
|
||||
if (str.length >= LINE_WIDTH) return str.substring(0, LINE_WIDTH);
|
||||
const pad = Math.floor((LINE_WIDTH - str.length) / 2);
|
||||
return ' '.repeat(pad) + str;
|
||||
};
|
||||
|
||||
const padLine = (left, right) => {
|
||||
left = left || '';
|
||||
right = right || '';
|
||||
const space = LINE_WIDTH - left.length - right.length;
|
||||
if (space <= 0) return left + ' ' + right;
|
||||
return left + ' '.repeat(space) + right;
|
||||
};
|
||||
|
||||
const divider = '='.repeat(LINE_WIDTH);
|
||||
const dashDivider = '-'.repeat(LINE_WIDTH);
|
||||
|
||||
let lines = [];
|
||||
lines.push(divider);
|
||||
lines.push(center(sale.store_name || 'HOSPITAL PHARMACY'));
|
||||
if (sale.building_name || sale.floor_no) {
|
||||
lines.push(center(`${sale.building_name || ''}, ${sale.floor_no || ''}`));
|
||||
}
|
||||
if (sale.store_address) {
|
||||
lines.push(center(sale.store_address));
|
||||
}
|
||||
if (sale.contact_phone) {
|
||||
lines.push(center(`Phone: ${sale.contact_phone}`));
|
||||
}
|
||||
if (sale.drug_license_no_20b || sale.drug_license_no_21b) {
|
||||
lines.push(center(`DL 20B: ${sale.drug_license_no_20b || 'N/A'} | 21B: ${sale.drug_license_no_21b || 'N/A'}`));
|
||||
}
|
||||
if (sale.gstin) {
|
||||
lines.push(center(`GSTIN: ${sale.gstin}${sale.state_code ? ` (State: ${sale.state_code})` : ''}`));
|
||||
}
|
||||
if (sale.fssai_no) {
|
||||
lines.push(center(`FSSAI: ${sale.fssai_no}`));
|
||||
}
|
||||
if (sale.abdm_hfr_id) {
|
||||
lines.push(center(`HFR ID: ${sale.abdm_hfr_id}`));
|
||||
}
|
||||
lines.push(divider);
|
||||
|
||||
lines.push(center('RETAIL TAX INVOICE'));
|
||||
lines.push(padLine(`Bill No: ${sale.invoice_no}`, `Date: ${sale.sale_date ? sale.sale_date.substring(0, 16) : ''}`));
|
||||
lines.push(padLine(`Patient: ${sale.patient_name || 'Walk-in'}`, `UHID: ${sale.uhid || 'Walk-in'}`));
|
||||
if (sale.patient_mobile) {
|
||||
lines.push(padLine(`Mobile : ${sale.patient_mobile}`, `Age/Sex: ${sale.age || 'N/A'}/${sale.gender || 'N/A'}`));
|
||||
}
|
||||
if (sale.doctor_name) {
|
||||
lines.push(`Doctor : ${sale.doctor_name}`);
|
||||
}
|
||||
if (sale.abha_id || sale.abha_address) {
|
||||
lines.push(`ABHA : ${sale.abha_id || ''} ${sale.abha_address ? `(${sale.abha_address})` : ''}`);
|
||||
}
|
||||
if (sale.abdm_care_context_ref) {
|
||||
lines.push(`ABDM Ref: ${sale.abdm_care_context_ref}`);
|
||||
}
|
||||
if (sale.ipd_id) {
|
||||
lines.push(`IPD # : ${sale.ipd_id} (Ward: ${sale.ward_name || ''} Bed: ${sale.bed_no || ''})`);
|
||||
}
|
||||
lines.push(dashDivider);
|
||||
|
||||
// Header: Item (22), Batch (9), Qty (7), Total (10)
|
||||
lines.push(
|
||||
'Item'.padEnd(22) +
|
||||
'Batch'.padEnd(9) +
|
||||
'Qty'.padStart(7) +
|
||||
'Total'.padStart(10)
|
||||
);
|
||||
lines.push(dashDivider);
|
||||
|
||||
if (items.length === 0) {
|
||||
lines.push(center('- Return / Refund Only -'));
|
||||
} else {
|
||||
items.forEach((it) => {
|
||||
const name = (it.item_name || '').substring(0, 21).padEnd(22);
|
||||
const batch = (it.batch_no || '').substring(0, 8).padEnd(9);
|
||||
const qtyStr = (it.sell_unit === 'Tablet'
|
||||
? `${it.total_units}T`
|
||||
: it.sell_unit === 'Combo'
|
||||
? `${it.qty}S+${it.loose_qty}T`
|
||||
: `${it.qty}`).substring(0, 6).padStart(7);
|
||||
const amtStr = Number(it.total_amount || 0).toFixed(2).padStart(10);
|
||||
lines.push(name + batch + qtyStr + amtStr);
|
||||
|
||||
let sub = ` Exp:${(it.expiry_date || '').substring(0, 7)} HSN:${it.hsn_code || '3004'} GST:${it.gst_rate}%`;
|
||||
if (it.sell_unit === 'Tablet') {
|
||||
sub += ` @Rs.${it.unit_price}/tab`;
|
||||
} else {
|
||||
sub += ` @Rs.${it.unit_mrp}`;
|
||||
}
|
||||
lines.push(sub.substring(0, LINE_WIDTH));
|
||||
});
|
||||
}
|
||||
|
||||
if (returnItems.length > 0) {
|
||||
lines.push(dashDivider);
|
||||
lines.push(center('-- RETURNED / EXCHANGED --'));
|
||||
if (inv.credit_note) {
|
||||
lines.push(center(`Credit Note: ${inv.credit_note.credit_note_no}`));
|
||||
}
|
||||
returnItems.forEach((it) => {
|
||||
const name = ('*' + (it.item_name || '')).substring(0, 21).padEnd(22);
|
||||
const batch = (it.batch_no || '').substring(0, 8).padEnd(9);
|
||||
const qtyStr = ('-' + (it.sell_unit === 'Tablet' ? `${it.total_units}T` : `${it.qty}`)).substring(0, 6).padStart(7);
|
||||
const amtStr = ('-' + Math.abs(Number(it.total_amount || 0)).toFixed(2)).padStart(10);
|
||||
lines.push(name + batch + qtyStr + amtStr);
|
||||
});
|
||||
}
|
||||
|
||||
lines.push(dashDivider);
|
||||
lines.push(padLine('Gross Total:', `Rs. ${Number(sale.gross_amount || 0).toFixed(2)}`));
|
||||
if (Number(sale.discount_amount) > 0) {
|
||||
lines.push(padLine('Discounts Allowed:', `-Rs. ${Number(sale.discount_amount).toFixed(2)}`));
|
||||
}
|
||||
if (Number(sale.return_amount) > 0) {
|
||||
lines.push(padLine('Less Return Credit:', `-Rs. ${Number(sale.return_amount).toFixed(2)}`));
|
||||
}
|
||||
lines.push(padLine('Taxable Amount:', `Rs. ${Number(sale.taxable_amount || 0).toFixed(2)}`));
|
||||
lines.push(padLine('CGST Amount:', `Rs. ${Number(sale.cgst_amount || 0).toFixed(2)}`));
|
||||
lines.push(padLine('SGST Amount:', `Rs. ${Number(sale.sgst_amount || 0).toFixed(2)}`));
|
||||
if (Number(sale.round_off) !== 0) {
|
||||
lines.push(padLine('Round Off:', `Rs. ${Number(sale.round_off || 0).toFixed(2)}`));
|
||||
}
|
||||
lines.push(divider);
|
||||
lines.push(padLine('NET PAYABLE:', `Rs. ${Number(sale.net_amount || 0).toFixed(2)}`));
|
||||
lines.push(divider);
|
||||
|
||||
if (Number(sale.refund_amount) > 0) {
|
||||
lines.push(padLine('REFUND PAID:', `Rs. ${Number(sale.refund_amount).toFixed(2)}`));
|
||||
lines.push(padLine('Refund Mode:', `${sale.refund_mode} ${sale.refund_ref_no || ''}`));
|
||||
lines.push(dashDivider);
|
||||
}
|
||||
|
||||
lines.push(`Settlement: ${sale.payment_mode}`);
|
||||
if (Number(sale.cash_paid) > 0) lines.push(` Cash Paid : Rs. ${Number(sale.cash_paid).toFixed(2)}`);
|
||||
if (Number(sale.upi_paid) > 0) lines.push(` UPI/Bank : Rs. ${Number(sale.upi_paid).toFixed(2)} ${sale.upi_ref_no ? `[UTR:${sale.upi_ref_no}]` : ''}`);
|
||||
if (Number(sale.card_paid) > 0) lines.push(` Card Paid : Rs. ${Number(sale.card_paid).toFixed(2)} ${sale.card_ref_no ? `[Ref:${sale.card_ref_no}]` : ''}`);
|
||||
if (Number(sale.credit_amount) > 0) lines.push(` IPD Credit: Rs. ${Number(sale.credit_amount).toFixed(2)}`);
|
||||
if (sale.is_bank_reconciled == 1) lines.push(' Status : Bank Statement Audited');
|
||||
else lines.push(' Status : Bank Statement Pending Audit');
|
||||
|
||||
lines.push(dashDivider);
|
||||
if (sale.registered_pharmacist_name) {
|
||||
lines.push(`R.Ph: ${sale.registered_pharmacist_name} (${sale.pharmacist_reg_no || 'N/A'})`);
|
||||
}
|
||||
lines.push(center(sale.terms_conditions || 'Thank you! Get well soon.'));
|
||||
lines.push(divider);
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
const copyThermalReceiptText = () => {
|
||||
if (!lastInvoice) return;
|
||||
const txt = generateThermalReceiptText(lastInvoice);
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(txt).then(() => {
|
||||
notify('Thermal receipt text copied to clipboard!', 'success');
|
||||
}).catch(() => {
|
||||
fallbackCopyText(txt);
|
||||
});
|
||||
} else {
|
||||
fallbackCopyText(txt);
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopyText = (txt) => {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = txt;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
notify('Thermal receipt text copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
notify('Unable to copy text.', 'error');
|
||||
}
|
||||
document.body.removeChild(ta);
|
||||
};
|
||||
|
||||
const printThermalReceipt = () => {
|
||||
if (!lastInvoice) return;
|
||||
const txt = generateThermalReceiptText(lastInvoice);
|
||||
const win = window.open('', '_blank', 'width=420,height=650');
|
||||
if (!win) {
|
||||
notify('Pop-up blocked. Please allow pop-ups to print thermal receipt.', 'warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
win.document.title = 'Thermal Receipt - ' + (lastInvoice.sale ? lastInvoice.sale.invoice_no : '');
|
||||
const style = win.document.createElement('style');
|
||||
style.textContent = '@page { size: 80mm auto; margin: 0; } html, body { margin: 0; padding: 0; background: #fff; } body { padding: 4mm 2mm; font-family: Courier, monospace; font-size: 11px; line-height: 1.25; color: #000; } pre { margin: 0; font-family: inherit; font-size: inherit; white-space: pre-wrap; word-break: break-all; }';
|
||||
win.document.head.appendChild(style);
|
||||
const pre = win.document.createElement('pre');
|
||||
pre.textContent = txt;
|
||||
win.document.body.appendChild(pre);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
win.focus();
|
||||
win.print();
|
||||
setTimeout(() => { try { win.close(); } catch (err) {} }, 1000);
|
||||
} catch (err) {}
|
||||
}, 250);
|
||||
} catch (e) {
|
||||
console.error('Print thermal error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const printPdf = (format) => {
|
||||
if (!lastInvoice || !lastInvoice.sale) return;
|
||||
const saleId = lastInvoice.sale.sale_id;
|
||||
const url = `/api/v1/medical-store/sales/invoice-pdf/${saleId}?format=${format || 'a4'}`;
|
||||
|
||||
let iframe = document.getElementById('pdf-print-iframe');
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.id = 'pdf-print-iframe';
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.right = '0';
|
||||
iframe.style.bottom = '0';
|
||||
iframe.style.width = '0';
|
||||
iframe.style.height = '0';
|
||||
iframe.style.border = '0';
|
||||
document.body.appendChild(iframe);
|
||||
}
|
||||
iframe.src = url;
|
||||
iframe.onload = function() {
|
||||
try {
|
||||
iframe.contentWindow.focus();
|
||||
iframe.contentWindow.print();
|
||||
} catch (e) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const downloadPdf = (format) => {
|
||||
if (!lastInvoice || !lastInvoice.sale) return;
|
||||
const saleId = lastInvoice.sale.sale_id;
|
||||
window.open(`/api/v1/medical-store/sales/invoice-pdf/${saleId}?format=${format || 'a4'}&download=1`, '_blank');
|
||||
};
|
||||
|
||||
// Global Keyboard Shortcuts (Desktop Pharmacy Standard)
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e) => {
|
||||
|
|
@ -2998,21 +3290,21 @@ function PosCounter({ store, notify }) {
|
|||
className={`btn ${printFormat === 'thermal' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
|
||||
onClick={() => setPrintFormat('thermal')}
|
||||
>
|
||||
Thermal 80mm (1-5 Items)
|
||||
<i className="bi bi-receipt me-1"></i> Thermal (Text)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${printFormat === 'a5' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
|
||||
onClick={() => setPrintFormat('a5')}
|
||||
>
|
||||
A5 Invoice (OPD)
|
||||
<i className="bi bi-file-earmark-pdf me-1"></i> A5 (PDF)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${printFormat === 'a4' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
|
||||
onClick={() => setPrintFormat('a4')}
|
||||
>
|
||||
A4 Tax Bill (IPD)
|
||||
<i className="bi bi-file-earmark-pdf me-1"></i> A4 (PDF)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3061,7 +3353,7 @@ function PosCounter({ store, notify }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-1.5">
|
||||
<div className="d-flex align-items-center gap-1.5 flex-wrap">
|
||||
<button
|
||||
className="btn btn-xs btn-outline-success text-white py-1 px-2"
|
||||
onClick={() => viewAbdmBundle(lastInvoice.sale.sale_id)}
|
||||
|
|
@ -3071,13 +3363,57 @@ function PosCounter({ store, notify }) {
|
|||
<i className="bi bi-filetype-json me-1"></i> FHIR R4
|
||||
</button>
|
||||
<div className="btn-group btn-group-sm">
|
||||
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'thermal' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('thermal')}>80mm</button>
|
||||
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'a5' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('a5')}>A5</button>
|
||||
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'a4' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('a4')}>A4</button>
|
||||
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'thermal' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('thermal')}>
|
||||
<i className="bi bi-receipt me-1"></i> Thermal (Text)
|
||||
</button>
|
||||
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'a5' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('a5')}>
|
||||
<i className="bi bi-file-earmark-pdf me-1"></i> A5 (PDF)
|
||||
</button>
|
||||
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'a4' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('a4')}>
|
||||
<i className="bi bi-file-earmark-pdf me-1"></i> A4 (PDF)
|
||||
</button>
|
||||
</div>
|
||||
<button className="btn btn-sm btn-primary py-1 px-2.5 ms-1 fw-bold" onClick={() => window.print()} title="Print Bill">
|
||||
<i className="bi bi-printer me-1"></i> Print
|
||||
</button>
|
||||
|
||||
{printFormat === 'thermal' ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-light py-1 px-2 text-white"
|
||||
onClick={copyThermalReceiptText}
|
||||
title="Copy plain-text receipt to clipboard"
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
>
|
||||
<i className="bi bi-clipboard me-1 text-warning"></i> Copy Text
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-warning text-dark fw-bold py-1 px-2.5 ms-0.5"
|
||||
onClick={printThermalReceipt}
|
||||
title="Print on 80mm Thermal Receipt Printer"
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
>
|
||||
<i className="bi bi-printer-fill me-1"></i> Print Thermal
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-light py-1 px-2 text-white"
|
||||
onClick={() => downloadPdf(printFormat)}
|
||||
title={`Download ${printFormat.toUpperCase()} PDF Document`}
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
>
|
||||
<i className="bi bi-download me-1 text-info"></i> PDF
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-primary py-1 px-2.5 ms-0.5 fw-bold"
|
||||
onClick={() => printPdf(printFormat)}
|
||||
title={`Print Vector-Clean ${printFormat.toUpperCase()} PDF`}
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
>
|
||||
<i className="bi bi-printer me-1"></i> Print PDF ({printFormat.toUpperCase()})
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-light rounded-pill px-2.5 py-1 ms-1 d-flex align-items-center gap-1"
|
||||
|
|
@ -3093,149 +3429,79 @@ function PosCounter({ store, notify }) {
|
|||
<div className="print-drawer-body">
|
||||
<div className="print-paper-card">
|
||||
<div id="printable-container">
|
||||
{/* Format 1: 80mm Thermal Slip (Single or 1-5 Items) */}
|
||||
{/* Format 1: 80mm Thermal Slip in Pure Text Layout */}
|
||||
{printFormat === 'thermal' && (
|
||||
<div className="print-thermal-80mm">
|
||||
<div className="text-center fw-bold fs-6">{lastInvoice.sale.store_name}</div>
|
||||
<div className="text-center">{lastInvoice.sale.building_name}, {lastInvoice.sale.floor_no}</div>
|
||||
{lastInvoice.sale.abdm_hfr_id && (
|
||||
<div className="text-center" style={{ fontSize: '9px' }}>HFR ID: {lastInvoice.sale.abdm_hfr_id}</div>
|
||||
)}
|
||||
<div className="text-center">DL 20B: {lastInvoice.sale.drug_license_no_20b}</div>
|
||||
<div className="text-center">DL 21B: {lastInvoice.sale.drug_license_no_21b}</div>
|
||||
<div className="text-center">GSTIN: {lastInvoice.sale.gstin}</div>
|
||||
<hr style={{ margin: '4px 0' }} />
|
||||
<div>Bill No: <strong>{lastInvoice.sale.invoice_no}</strong></div>
|
||||
<div>Date: {lastInvoice.sale.sale_date}</div>
|
||||
<div>Patient: {lastInvoice.sale.patient_name} {lastInvoice.sale.uhid ? `(${lastInvoice.sale.uhid})` : ''}</div>
|
||||
{(lastInvoice.sale.abha_id || lastInvoice.sale.abha_address) && (
|
||||
<div style={{ fontSize: '9px' }}>ABHA: {lastInvoice.sale.abha_id || ''} {lastInvoice.sale.abha_address ? `(${lastInvoice.sale.abha_address})` : ''}</div>
|
||||
)}
|
||||
{lastInvoice.sale.abdm_care_context_ref && (
|
||||
<div style={{ fontSize: '8px' }}>Care Context: {lastInvoice.sale.abdm_care_context_ref}</div>
|
||||
)}
|
||||
<div>Doctor: {lastInvoice.sale.doctor_name}</div>
|
||||
<hr style={{ margin: '4px 0' }} />
|
||||
<table style={{ width: '100%', fontSize: '10px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }}>Item</th>
|
||||
<th style={{ textAlign: 'center' }}>Batch</th>
|
||||
<th style={{ textAlign: 'center' }}>Qty</th>
|
||||
<th style={{ textAlign: 'right' }}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lastInvoice.items.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: 'center', color: '#666', padding: '6px 0' }}>
|
||||
- Return / Refund Only (No New Meds) -
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
lastInvoice.items.map((it, i) => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
{it.item_name}
|
||||
{it.sell_unit === 'Tablet' && (
|
||||
<div style={{ fontSize: '8px', color: '#666' }}>@ ₹{it.unit_price}/tab</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>{it.batch_no}</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
{it.sell_unit === 'Tablet'
|
||||
? `${it.total_units} Tab`
|
||||
: it.sell_unit === 'Combo'
|
||||
? `${it.qty} Str + ${it.loose_qty} Tab`
|
||||
: (Number(it.units_per_pack) > 1 ? `${it.qty} Strip` : `${it.qty} ${it.unit_pack || 'Unit'}`)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right' }}>₹{it.total_amount}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{lastInvoice.return_items && lastInvoice.return_items.length > 0 && (
|
||||
<>
|
||||
<div style={{ fontSize: '9px', fontWeight: 'bold', margin: '4px 0 2px', borderTop: '1px dashed #000', paddingTop: 2 }}>
|
||||
-- RETURNED / EXCHANGED --
|
||||
{lastInvoice.credit_note && <span style={{ float: 'right' }}>CN: {lastInvoice.credit_note.credit_note_no}</span>}
|
||||
</div>
|
||||
<table style={{ width: '100%', fontSize: '9px' }}>
|
||||
<tbody>
|
||||
{lastInvoice.return_items.map((it, i) => (
|
||||
<tr key={`ret-${i}`}>
|
||||
<td>
|
||||
<span style={{ textDecoration: 'line-through' }}>{it.item_name}</span>
|
||||
<div style={{ fontSize: '8px', color: '#666' }}>
|
||||
{it.ref_invoice_no ? `Ref: ${it.ref_invoice_no}` : ''} ({it.return_condition})
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>{it.batch_no}</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
-{it.sell_unit === 'Tablet' ? `${it.total_units} Tab` : `${it.qty} ${it.unit_pack || 'Unit'}`}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', color: '#b91c1c' }}>-₹{Math.abs(Number(it.total_amount)).toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
<hr style={{ margin: '4px 0' }} />
|
||||
<div className="d-flex justify-content-between" style={{ fontSize: '10px' }}>
|
||||
<span>Gross Total:</span>
|
||||
<span>₹{lastInvoice.sale.gross_amount}</span>
|
||||
</div>
|
||||
{Number(lastInvoice.sale.discount_amount) > 0 && (
|
||||
<div className="d-flex justify-content-between text-danger" style={{ fontSize: '10px' }}>
|
||||
<span>Total Discount:</span>
|
||||
<span>-₹{lastInvoice.sale.discount_amount}</span>
|
||||
<div className="p-2 text-center">
|
||||
<div className="d-flex justify-content-between align-items-center mb-2 mx-auto" style={{ maxWidth: '360px' }}>
|
||||
<span className="badge bg-dark text-warning border border-secondary" style={{ fontSize: '0.72rem' }}>
|
||||
<i className="bi bi-receipt me-1"></i> 80mm Thermal Receipt (Plain Text)
|
||||
</span>
|
||||
<div className="d-flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-outline-secondary text-dark bg-light py-0.5 px-2"
|
||||
onClick={copyThermalReceiptText}
|
||||
title="Copy receipt text to clipboard"
|
||||
style={{ fontSize: '0.72rem' }}
|
||||
>
|
||||
<i className="bi bi-clipboard me-1 text-primary"></i> Copy Text
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-warning text-dark fw-bold py-0.5 px-2"
|
||||
onClick={printThermalReceipt}
|
||||
title="Print Thermal 80mm Receipt"
|
||||
style={{ fontSize: '0.72rem' }}
|
||||
>
|
||||
<i className="bi bi-printer-fill me-1"></i> Print Thermal
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{Number(lastInvoice.sale.return_amount) > 0 && (
|
||||
<div className="d-flex justify-content-between text-danger fw-bold" style={{ fontSize: '10px' }}>
|
||||
<span>Less Return Credit:</span>
|
||||
<span>-₹{lastInvoice.sale.return_amount}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex justify-content-between fw-bold" style={{ fontSize: '12px' }}>
|
||||
<span>NET PAYABLE:</span>
|
||||
<span>₹{lastInvoice.sale.net_amount}</span>
|
||||
</div>
|
||||
{Number(lastInvoice.sale.refund_amount) > 0 && (
|
||||
<div className="d-flex justify-content-between fw-bold text-danger border-top border-bottom py-1 my-1" style={{ fontSize: '11px' }}>
|
||||
<span>REFUND PAID TO PATIENT:</span>
|
||||
<span>₹{lastInvoice.sale.refund_amount} ({lastInvoice.sale.refund_mode})</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: '9px', marginTop: 4 }}>
|
||||
<div><strong>Settlement:</strong> {lastInvoice.sale.payment_mode}</div>
|
||||
{Number(lastInvoice.sale.cash_paid) > 0 && <div>• Cash Paid: ₹{lastInvoice.sale.cash_paid}</div>}
|
||||
{Number(lastInvoice.sale.upi_paid) > 0 && (
|
||||
<div>• UPI/Bank: ₹{lastInvoice.sale.upi_paid} {lastInvoice.sale.upi_ref_no ? `[UTR: ${lastInvoice.sale.upi_ref_no}]` : ''} {lastInvoice.sale.bank_name ? `(${lastInvoice.sale.bank_name})` : ''}</div>
|
||||
)}
|
||||
{Number(lastInvoice.sale.card_paid) > 0 && (
|
||||
<div>• Card Paid: ₹{lastInvoice.sale.card_paid} {lastInvoice.sale.card_ref_no ? `[Ref: ${lastInvoice.sale.card_ref_no}]` : ''}</div>
|
||||
)}
|
||||
{Number(lastInvoice.sale.credit_amount) > 0 && <div>• IPD Credit: ₹{lastInvoice.sale.credit_amount}</div>}
|
||||
{Number(lastInvoice.sale.refund_amount) > 0 && (
|
||||
<div className="text-danger fw-bold">• Refund Issued: ₹{lastInvoice.sale.refund_amount} via {lastInvoice.sale.refund_mode} {lastInvoice.sale.refund_ref_no ? `[Ref: ${lastInvoice.sale.refund_ref_no}]` : ''}</div>
|
||||
)}
|
||||
{lastInvoice.sale.is_bank_reconciled == 1 && <div className="text-success fw-bold">✓ Bank Statement Audited</div>}
|
||||
</div>
|
||||
<div style={{ fontSize: '9px', marginTop: 4, textAlign: 'center' }}>
|
||||
R.Ph: {lastInvoice.sale.registered_pharmacist_name || 'Pharmacist'} {lastInvoice.sale.pharmacist_hpr_id ? `(HPR: ${lastInvoice.sale.pharmacist_hpr_id})` : ''}
|
||||
</div>
|
||||
<div style={{ fontSize: '8px', textAlign: 'center', marginTop: 4 }}>
|
||||
{lastInvoice.sale.terms_conditions || 'Thank you! Get well soon.'}
|
||||
</div>
|
||||
<pre
|
||||
id="thermal-receipt-preview"
|
||||
style={{
|
||||
fontFamily: "'Courier New', Courier, monospace",
|
||||
fontSize: '11px',
|
||||
lineHeight: '1.28',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#000000',
|
||||
padding: '16px',
|
||||
border: '1px solid #cbd5e1',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.06)',
|
||||
width: '360px',
|
||||
maxWidth: '100%',
|
||||
margin: '0 auto',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all'
|
||||
}}
|
||||
>
|
||||
{generateThermalReceiptText(lastInvoice)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Format 2: A5 Half-Page Invoice (Normal OPD/Walk-in) */}
|
||||
{printFormat === 'a5' && (
|
||||
<div className="print-a5-invoice">
|
||||
<div className="d-flex justify-content-between align-items-center bg-primary-subtle border border-primary-subtle rounded px-2.5 py-1.5 mb-2.5 no-print">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<i className="bi bi-file-earmark-pdf-fill text-danger fs-5"></i>
|
||||
<div>
|
||||
<div className="fw-bold text-dark" style={{ fontSize: '0.78rem' }}>A5 Retail Tax Invoice (PDF Ready)</div>
|
||||
<small className="text-muted" style={{ fontSize: '0.68rem' }}>Direct server vector PDF • Zero clipped columns</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-1.5">
|
||||
<button className="btn btn-xs btn-outline-primary py-1 px-2" onClick={() => downloadPdf('a5')} style={{ fontSize: '0.72rem' }}>
|
||||
<i className="bi bi-download me-1"></i> Download PDF
|
||||
</button>
|
||||
<button className="btn btn-xs btn-primary py-1 px-2.5 fw-bold" onClick={() => printPdf('a5')} style={{ fontSize: '0.72rem' }}>
|
||||
<i className="bi bi-printer me-1"></i> Print PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-start border-bottom pb-2 mb-2">
|
||||
<div>
|
||||
<h6 className="fw-bold mb-0 text-primary">{lastInvoice.sale.store_name}</h6>
|
||||
|
|
@ -3407,6 +3673,23 @@ function PosCounter({ store, notify }) {
|
|||
{/* Format 3: A4 Comprehensive Hospital Tax Invoice (IPD & Large Bills) */}
|
||||
{printFormat === 'a4' && (
|
||||
<div className="print-a4-invoice">
|
||||
<div className="d-flex justify-content-between align-items-center bg-primary-subtle border border-primary-subtle rounded px-3 py-1.5 mb-3 no-print">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<i className="bi bi-file-earmark-pdf-fill text-danger fs-5"></i>
|
||||
<div>
|
||||
<div className="fw-bold text-dark" style={{ fontSize: '0.8rem' }}>A4 Hospital GST Tax Invoice (PDF Ready)</div>
|
||||
<small className="text-muted" style={{ fontSize: '0.68rem' }}>Server-rendered vector PDF • Zero clipped columns</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-1.5">
|
||||
<button className="btn btn-xs btn-outline-primary py-1 px-2.5" onClick={() => downloadPdf('a4')} style={{ fontSize: '0.72rem' }}>
|
||||
<i className="bi bi-download me-1"></i> Download PDF
|
||||
</button>
|
||||
<button className="btn btn-xs btn-primary py-1 px-2.5 fw-bold" onClick={() => printPdf('a4')} style={{ fontSize: '0.72rem' }}>
|
||||
<i className="bi bi-printer me-1"></i> Print PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center border-bottom pb-3 mb-3">
|
||||
<div>
|
||||
<h4 className="fw-bold text-primary mb-1">{lastInvoice.sale.store_name}</h4>
|
||||
|
|
@ -3709,13 +3992,29 @@ function PosCounter({ store, notify }) {
|
|||
<span className="text-muted small">
|
||||
Format: <strong className="text-dark text-uppercase">{printFormat}</strong> • Bill No: <strong>{lastInvoice.sale.invoice_no}</strong>
|
||||
</span>
|
||||
<div className="d-flex gap-2">
|
||||
<div className="d-flex gap-2 flex-wrap align-items-center">
|
||||
<button className="btn btn-sm btn-outline-secondary px-3" onClick={() => setIsPrintOpen(false)}>
|
||||
Done / Close (Esc)
|
||||
</button>
|
||||
<button className="btn btn-sm btn-primary px-3 fw-bold" onClick={() => window.print()}>
|
||||
<i className="bi bi-printer me-1"></i> Print Bill ({printFormat.toUpperCase()})
|
||||
</button>
|
||||
{printFormat === 'thermal' ? (
|
||||
<>
|
||||
<button className="btn btn-sm btn-outline-dark px-3" onClick={copyThermalReceiptText}>
|
||||
<i className="bi bi-clipboard me-1 text-primary"></i> Copy Text
|
||||
</button>
|
||||
<button className="btn btn-sm btn-warning text-dark px-3 fw-bold" onClick={printThermalReceipt}>
|
||||
<i className="bi bi-printer-fill me-1"></i> Print Thermal Receipt
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="btn btn-sm btn-outline-primary px-3" onClick={() => downloadPdf(printFormat)}>
|
||||
<i className="bi bi-download me-1"></i> Download PDF
|
||||
</button>
|
||||
<button className="btn btn-sm btn-primary px-3 fw-bold" onClick={() => printPdf(printFormat)}>
|
||||
<i className="bi bi-printer me-1"></i> Print PDF ({printFormat.toUpperCase()})
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user