diff --git a/package.json b/package.json index 6e6b78a..de16dd2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "exceljs": "^4.4.0", "framer-motion": "^12.40.0", "gsap": "^3.15.0", + "idb": "^8.0.3", "lucide-react": "^1.17.0", "next": "^16.2.6", "next-auth": "^5.0.0-beta.31", diff --git a/src/lib/offline/db.ts b/src/lib/offline/db.ts new file mode 100644 index 0000000..de7d5b1 --- /dev/null +++ b/src/lib/offline/db.ts @@ -0,0 +1,50 @@ +import { openDB, type DBSchema, type IDBPDatabase } from "idb"; + +export interface QueuedAction { + id: string; // clientActionId (uuid v4) + actionName: string; // e.g. "markOrderReady" + payload: unknown; // JSON-serializable args + createdAt: number; // Date.now() + attempts: number; // sync attempts + lastAttemptAt: number | null; + status: "pending" | "in-flight" | "synced" | "conflict" | "failed"; + error?: string; +} + +interface RouteCommerceOfflineDB extends DBSchema { + queue: { + key: string; + value: QueuedAction; + indexes: { "by-status": string; "by-created": number }; + }; + cache: { + key: string; // e.g. "orders:brandId:abc" + value: { key: string; data: unknown; cachedAt: number }; + }; +} + +const DB_NAME = "route-commerce-offline"; +const DB_VERSION = 1; + +let dbPromise: Promise> | null = null; + +export function getOfflineDB(): Promise> { + if (typeof window === "undefined") { + throw new Error("getOfflineDB called on the server"); + } + if (!dbPromise) { + dbPromise = openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains("queue")) { + const queue = db.createObjectStore("queue", { keyPath: "id" }); + queue.createIndex("by-status", "status"); + queue.createIndex("by-created", "createdAt"); + } + if (!db.objectStoreNames.contains("cache")) { + db.createObjectStore("cache", { keyPath: "key" }); + } + }, + }); + } + return dbPromise; +}