feat(dashboard): Next.js 16 merchant panel with offline POS and PWA

Complete merchant dashboard upgrade:

Next.js 16 compatibility:
- Fix params/searchParams typed as Promise<{}> throughout App Router
- Replace middleware.ts with proxy.ts (Next.js 16 convention)
- Remove unused @ts-expect-error directives caught by stricter TS
- Cast dynamic next-intl t() keys to fix TranslateArgs type errors

Offline POS:
- IndexedDB queue (meezi_pos_offline) for orders created while offline
- Zustand sync store tracking queueCount, isSyncing, isOnline
- useOfflineSync hook: auto-syncs on reconnect/visibility-change
- SyncStatusIndicator chip in topbar (amber=offline, blue=syncing)
- submitOrderToApi falls back to local order on network failure
- Local orders skip payment flow; sync on reconnect

PWA (installable):
- @ducanh2912/next-pwa with Workbox runtime caching rules
- Web App Manifest (manifest.ts) — RTL/Farsi, theme #0F6E56
- PWA icons: 192px, 512px, maskable 512px
- next.config.ts replaces next.config.mjs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-27 21:34:12 +03:30
parent ef15fd6247
commit 131ecdbbe6
208 changed files with 37123 additions and 0 deletions
@@ -0,0 +1,114 @@
"use client";
import {
createContext,
useCallback,
useContext,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { useTranslations } from "next-intl";
import { TriangleAlert } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { cn } from "@/lib/utils";
export type ConfirmOptions = {
title?: string;
description: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: "default" | "destructive";
};
type ConfirmContextValue = {
confirm: (options: ConfirmOptions) => Promise<boolean>;
};
const ConfirmContext = createContext<ConfirmContextValue | null>(null);
export function ConfirmProvider({ children }: { children: ReactNode }) {
const t = useTranslations("confirm");
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<ConfirmOptions | null>(null);
const resolveRef = useRef<((value: boolean) => void) | null>(null);
const confirm = useCallback((opts: ConfirmOptions) => {
setOptions(opts);
setOpen(true);
return new Promise<boolean>((resolve) => {
resolveRef.current = resolve;
});
}, []);
const finish = useCallback((value: boolean) => {
setOpen(false);
resolveRef.current?.(value);
resolveRef.current = null;
setTimeout(() => setOptions(null), 200);
}, []);
const value = useMemo(() => ({ confirm }), [confirm]);
const isDestructive = options?.variant === "destructive";
return (
<ConfirmContext.Provider value={value}>
{children}
<AlertDialog open={open} onOpenChange={(next) => !next && finish(false)}>
<AlertDialogContent className="max-w-md">
<AlertDialogHeader>
<div className="flex items-start gap-3 sm:text-start">
<span
className={cn(
"flex h-10 w-10 shrink-0 items-center justify-center rounded-full",
isDestructive ? "bg-red-50 text-[#A32D2D]" : "bg-[#E1F5EE] text-[#0F6E56]"
)}
>
<TriangleAlert className="h-5 w-5" />
</span>
<div className="min-w-0 space-y-1.5 pt-0.5">
<AlertDialogTitle>
{options?.title ?? t("title")}
</AlertDialogTitle>
<AlertDialogDescription>{options?.description}</AlertDialogDescription>
</div>
</div>
</AlertDialogHeader>
<AlertDialogFooter className="sm:justify-end">
<AlertDialogCancel onClick={() => finish(false)}>
{options?.cancelLabel ?? t("cancel")}
</AlertDialogCancel>
<AlertDialogAction
className={cn(
isDestructive &&
"bg-destructive text-destructive-foreground hover:opacity-90"
)}
onClick={() => finish(true)}
>
{options?.confirmLabel ?? t("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ConfirmContext.Provider>
);
}
export function useConfirm() {
const ctx = useContext(ConfirmContext);
if (!ctx) {
throw new Error("useConfirm must be used within ConfirmProvider");
}
return ctx.confirm;
}