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,60 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { StockLevel } from "@/lib/types";
|
||||
|
||||
interface Props {
|
||||
levels: StockLevel[];
|
||||
}
|
||||
|
||||
/** Current stock per branch at a glance, with anything low or out listed first. */
|
||||
export default function StockSummaryCard({ levels }: Props) {
|
||||
const totalsByBranch = new Map<string, { name: string; units: number; alerts: number }>();
|
||||
for (const l of levels) {
|
||||
const entry = totalsByBranch.get(l.branchId) ?? { name: l.branchName, units: 0, alerts: 0 };
|
||||
entry.units += Math.max(l.quantity, 0);
|
||||
if (l.status !== "OK") entry.alerts += 1;
|
||||
totalsByBranch.set(l.branchId, entry);
|
||||
}
|
||||
const alerts = levels.filter((l) => l.status !== "OK").sort((a, b) => a.quantity - b.quantity);
|
||||
|
||||
return (
|
||||
<section className="rounded-xl2 bg-white p-4 shadow-soft">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="font-display font-semibold text-depth-900">Stock on hand</h2>
|
||||
<Link href="/admin/stock" className="text-sm font-medium text-flow-600 underline underline-offset-2">
|
||||
Manage stock
|
||||
</Link>
|
||||
</div>
|
||||
{levels.length === 0 ? (
|
||||
<p className="text-sm text-depth-500">No stock recorded yet.</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{Array.from(totalsByBranch.entries()).map(([id, b]) => (
|
||||
<div key={id} className="rounded-xl bg-sand-50 px-3 py-2">
|
||||
<p className="truncate text-xs text-depth-500">{b.name}</p>
|
||||
<p className="font-display text-lg font-bold text-depth-900">{b.units.toLocaleString()} units</p>
|
||||
{b.alerts > 0 && <p className="text-xs font-medium text-cash-700">{b.alerts} low/out</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{alerts.length > 0 && (
|
||||
<ul className="mt-3 space-y-1 text-sm">
|
||||
{alerts.slice(0, 6).map((l) => (
|
||||
<li key={l.branchId + l.productId} className="flex justify-between gap-3">
|
||||
<span className="truncate text-depth-700">
|
||||
{l.productLabel} · {l.branchName}
|
||||
</span>
|
||||
<span className={"shrink-0 font-mono " + (l.status === "OUT" ? "text-alert-600" : "text-cash-700")}>
|
||||
{l.quantity}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user