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,51 @@
|
||||
"use client";
|
||||
|
||||
import { Product } from "@/lib/types";
|
||||
|
||||
interface Props {
|
||||
products: Product[];
|
||||
quantities: Record<string, number>;
|
||||
onAdd: (productId: string) => void;
|
||||
}
|
||||
|
||||
/** One tap adds one unit of a product to the current sale. */
|
||||
export default function ProductGrid({ products, quantities, onAdd }: Props) {
|
||||
if (products.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl2 border border-dashed border-depth-200 bg-white p-4 text-center text-sm text-depth-500">
|
||||
No products configured yet. Ask the owner to add them.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="mb-1.5 block text-sm font-medium text-depth-600">Tap a product to add it</span>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
{products.map((p) => {
|
||||
const inCart = quantities[p._id] ?? 0;
|
||||
return (
|
||||
<button
|
||||
key={p._id}
|
||||
type="button"
|
||||
onClick={() => onAdd(p._id)}
|
||||
aria-label={`Add ${p.label}`}
|
||||
className={
|
||||
"tap-target relative flex flex-col items-start justify-center rounded-xl2 border-2 px-3 py-2 text-left transition active:scale-[0.97] " +
|
||||
(inCart > 0 ? "border-flow-500 bg-flow-500/10" : "border-depth-200 bg-white")
|
||||
}
|
||||
>
|
||||
<span className="font-display text-sm font-bold leading-tight text-depth-900">{p.label}</span>
|
||||
<span className="font-mono text-xs text-depth-500">KES {p.unitPrice.toLocaleString()}</span>
|
||||
{inCart > 0 && (
|
||||
<span className="absolute right-2 top-2 flex h-6 min-w-6 items-center justify-center rounded-full bg-flow-500 px-1.5 font-mono text-xs font-bold text-white">
|
||||
{inCart}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user