Make the terminal work offline end to end, and add stock screens
Terminal (PRD 5.3/5.5): - Opens with no network: a service worker caches the app shell and build assets, and the last confirmed session and product list are kept on the phone. Only a definitive 401/403 sends the cashier to the login page. - The offline queue is keyed by cashier and branch, sends only the signed-in cashier's sales, chunks to the server's batch limit, and acts on the per-sale result: synced, or rejected with the reason shown and never retried. Rows from the old queue are adopted on first sync. - Several products per sale (tap to add), optional customer details, and a printable receipt. The banner shows sales waiting to sync and any that need attention; sign-out warns about unsynced sales. - M-Pesa only appears when FEATURE_MPESA is on. A timed-out prompt now tells the cashier not to take cash until the payment is checked, instead of reporting it as failed. Admin: - New Stock page: stock per branch with low/out flags, physical counts and adjustments, inter-branch transfers, morning dispatch and evening count entry, and recent movements. - Dashboard shows stock on hand per branch and links to Stock. - Staff page can deactivate and reactivate accounts. - Products page is read-only for branch managers and edits the low-stock alert level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import type { QueuedSale } from "@/lib/offlineQueue";
|
||||
|
||||
interface Props {
|
||||
sale: QueuedSale;
|
||||
businessName?: string | null;
|
||||
branchName?: string | null;
|
||||
cashierName: string;
|
||||
synced: boolean;
|
||||
}
|
||||
|
||||
/** A printable sale receipt. Printing hides everything but `.print-receipt` (see globals.css). */
|
||||
export default function Receipt({ sale, businessName, branchName, cashierName, synced }: Props) {
|
||||
const when = new Date(sale.clientSubmittedAt);
|
||||
const reference = sale.clientUuid.slice(0, 8).toUpperCase();
|
||||
|
||||
return (
|
||||
<div className="print-receipt rounded-xl2 border border-depth-200/60 bg-white p-5 font-mono text-sm text-depth-900 shadow-soft">
|
||||
<div className="text-center">
|
||||
<p className="font-display text-base font-bold">{businessName ?? "Receipt"}</p>
|
||||
{branchName && <p className="text-xs text-depth-500">{branchName}</p>}
|
||||
<p className="mt-1 text-xs text-depth-500">
|
||||
{when.toLocaleDateString("en-KE")} {when.toLocaleTimeString("en-KE", { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
<p className="text-xs text-depth-500">Ref {reference}</p>
|
||||
</div>
|
||||
|
||||
<table className="mt-4 w-full">
|
||||
<tbody>
|
||||
{sale.lines.map((l) => (
|
||||
<tr key={l.productId} className="align-top">
|
||||
<td className="py-0.5 pr-2">
|
||||
{l.quantity} x {l.label}
|
||||
<span className="block text-xs text-depth-500">@ KES {l.unitPrice.toLocaleString()}</span>
|
||||
</td>
|
||||
<td className="py-0.5 text-right">{(l.quantity * l.unitPrice).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="mt-3 flex justify-between border-t border-dashed border-depth-300 pt-2 font-bold">
|
||||
<span>TOTAL (KES)</span>
|
||||
<span>{sale.total.toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs">Paid: Cash</p>
|
||||
{sale.customerName && <p className="text-xs">Customer: {sale.customerName}</p>}
|
||||
<p className="text-xs">Served by: {cashierName}</p>
|
||||
|
||||
<p className="no-print mt-3 text-center text-xs text-depth-500">
|
||||
{synced ? "Synced to head office" : "Saved on this phone - will sync automatically"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user