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:
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { apiFetch, ApiError } from "@/lib/api";
|
||||
import { useTransactionFeed } from "@/lib/pollingFeed";
|
||||
import { AuthUser, Branch, DashboardSummary, ReconciliationRow } from "@/lib/types";
|
||||
import { AuthUser, Branch, DashboardSummary, ReconciliationRow, StockLevel } from "@/lib/types";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { SkeletonCardGrid, SkeletonTable } from "@/components/ui/Skeleton";
|
||||
import EmptyState from "@/components/ui/EmptyState";
|
||||
@@ -16,6 +16,7 @@ import LiveStatsRow from "@/components/admin/LiveStatsRow";
|
||||
import BranchRankingList from "@/components/admin/BranchRankingList";
|
||||
import ActivityFeedMatrix from "@/components/admin/ActivityFeedMatrix";
|
||||
import ReconciliationGrid from "@/components/admin/ReconciliationGrid";
|
||||
import StockSummaryCard from "@/components/admin/StockSummaryCard";
|
||||
|
||||
export default function AdminDashboardPage() {
|
||||
const router = useRouter();
|
||||
@@ -27,6 +28,7 @@ export default function AdminDashboardPage() {
|
||||
const [selectedBranchId, setSelectedBranchId] = useState<string>("ALL");
|
||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||
const [reconciliation, setReconciliation] = useState<ReconciliationRow[]>([]);
|
||||
const [stock, setStock] = useState<StockLevel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const { events: feedTransactions, connected } = useTransactionFeed(selectedBranchId, 50);
|
||||
@@ -75,12 +77,14 @@ export default function AdminDashboardPage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const branchParam = selectedBranchId === "ALL" ? "" : "?branchId=" + selectedBranchId;
|
||||
const [summaryRes, reconRes] = await Promise.all([
|
||||
const [summaryRes, reconRes, stockRes] = await Promise.all([
|
||||
apiFetch<DashboardSummary>("/api/dashboard/summary" + branchParam),
|
||||
apiFetch<{ rows: ReconciliationRow[] }>("/api/dashboard/reconciliation" + branchParam),
|
||||
apiFetch<{ levels: StockLevel[] }>("/api/stock" + branchParam),
|
||||
]);
|
||||
setSummary(summaryRes);
|
||||
setReconciliation(reconRes.rows);
|
||||
setStock(stockRes.levels);
|
||||
} catch (err) {
|
||||
show(err instanceof ApiError ? err.message : "Could not load dashboard data", "error");
|
||||
}
|
||||
@@ -138,6 +142,9 @@ export default function AdminDashboardPage() {
|
||||
Platform
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/admin/stock" className="tap-target flex items-center rounded-xl2 bg-white/10 px-4 text-sm font-medium text-white hover:bg-white/15">
|
||||
Stock
|
||||
</Link>
|
||||
<Link href="/admin/products" className="tap-target flex items-center rounded-xl2 bg-white/10 px-4 text-sm font-medium text-white hover:bg-white/15">
|
||||
Sizes
|
||||
</Link>
|
||||
@@ -183,6 +190,8 @@ export default function AdminDashboardPage() {
|
||||
pendingMpesa={summary.totals.pendingMpesa}
|
||||
/>
|
||||
|
||||
<StockSummaryCard levels={stock} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<PaymentSplitDonut cashTotal={summary.totals.cashTotal} mpesaTotal={summary.totals.mpesaTotal} />
|
||||
<LiveStatsRow
|
||||
|
||||
Reference in New Issue
Block a user