Files
meezi/web/dashboard/src/lib/api/admin-client.ts
T
soroush.asadi 131ecdbbe6 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>
2026-05-27 21:34:12 +03:30

88 lines
2.7 KiB
TypeScript

import axios, { type AxiosError } from "axios";
import type { ApiResponse } from "./types";
const baseURL =
process.env.NEXT_PUBLIC_ADMIN_API_URL ??
process.env.NEXT_PUBLIC_API_URL ??
"https://localhost:7210";
export const adminApi = axios.create({
baseURL,
headers: { "Content-Type": "application/json" },
});
adminApi.interceptors.request.use((config) => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("meezi_admin_access_token");
if (token) config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
adminApi.interceptors.response.use(
(response) => response,
(error: AxiosError<ApiResponse<unknown>>) => {
const apiError = error.response?.data?.error;
if (apiError?.code) {
return Promise.reject(new AdminApiClientError(apiError.code, apiError.message));
}
if (error.response?.status === 401 && typeof window !== "undefined") {
localStorage.removeItem("meezi_admin_access_token");
localStorage.removeItem("meezi_admin_refresh_token");
localStorage.removeItem("meezi_admin_auth");
const locale = window.location.pathname.split("/")[1] ?? "fa";
window.location.href = `/${locale}/admin/login`;
}
return Promise.reject(error);
}
);
export class AdminApiClientError extends Error {
constructor(
public readonly code: string,
message: string
) {
super(message);
this.name = "AdminApiClientError";
}
}
export async function adminGet<T>(url: string): Promise<T> {
const { data } = await adminApi.get<ApiResponse<T>>(url);
if (!data.success || data.data === undefined) {
throw new Error(data.error?.message ?? "Request failed");
}
return data.data;
}
export async function adminPost<T>(url: string, body?: unknown): Promise<T> {
const { data } = await adminApi.post<ApiResponse<T>>(url, body);
if (!data.success || data.data === undefined) {
throw new Error(data.error?.message ?? "Request failed");
}
return data.data;
}
export async function adminPut<T>(url: string, body: unknown): Promise<T> {
const { data } = await adminApi.put<ApiResponse<T>>(url, body);
if (!data.success || data.data === undefined) {
throw new Error(data.error?.message ?? "Request failed");
}
return data.data;
}
export async function adminPatch<T>(url: string, body: unknown): Promise<T> {
const { data } = await adminApi.patch<ApiResponse<T>>(url, body);
if (!data.success || data.data === undefined) {
throw new Error(data.error?.message ?? "Request failed");
}
return data.data;
}
export async function adminDelete(url: string): Promise<void> {
const { data } = await adminApi.delete<ApiResponse<unknown>>(url);
if (!data.success) {
throw new Error(data.error?.message ?? "Request failed");
}
}