From 11ab8908fa8cd969abf6550d481de346bb8b48fb Mon Sep 17 00:00:00 2001 From: "Najjar\\NajjarV02" Date: Fri, 17 Apr 2026 16:08:11 +0400 Subject: [PATCH] feat: enhance pricing and persona synchronization logic for improved data consistency --- src/app/admin/page.tsx | 75 +++++++++++++++++++++++++++++------- src/store/usePersonaStore.ts | 11 ++++-- src/store/usePricingStore.ts | 20 +++++++++- 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c23dc65..dd60b76 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -48,25 +48,74 @@ export default function AdminPage() { const res = await fetch('/api/admin/pricing/'); const data = await res.json(); const serverItems: { id: string; label: string; price: number; modelPath: string | null }[] = data.items ?? []; + const localItems = pricingStore.getState().items; + if (serverItems.length > 0) { - // Sync server data into local stores - serverItems.forEach(({ id, label, price, modelPath }) => { - const existing = pricingStore.getState().items.find((i) => i.id === id); - if (existing) { - pricingStore.getState().updateItem(id, { label, price, modelPath: modelPath ?? undefined }); - } else { - pricingStore.getState().addItem({ id, label, price, modelPath: modelPath ?? undefined }); + // Merge: server wins for label/price, but keep whichever modelPath exists + const serverMap = new Map(serverItems.map((s) => [s.id, s])); + const localMap = new Map(localItems.map((l) => [l.id, l])); + const itemsToUpdate: { id: string; label: string; price: number; modelPath?: string }[] = []; + + // Merge server items with local modelPaths + for (const server of serverItems) { + const local = localMap.get(server.id); + // If local has a modelPath but server doesn't, push it to server + const mergedModelPath = server.modelPath || local?.modelPath; + pricingStore.getState().updateItem(server.id, { + label: server.label, + price: server.price, + modelPath: mergedModelPath ?? undefined, + }); + // If we don't already have this item locally, add it + if (!local) { + pricingStore.getState().addItem({ + id: server.id, + label: server.label, + price: server.price, + modelPath: mergedModelPath ?? undefined, + }); + } + // If local had a modelPath but server didn't, push to server + if (!server.modelPath && local?.modelPath) { + itemsToUpdate.push({ id: server.id, label: server.label, price: server.price, modelPath: local.modelPath }); } // Sync persona store - if (modelPath) { - const existingPersona = personaStore.getState().personas.find((p) => p.id === id); + if (mergedModelPath && server.id !== 'base') { + const existingPersona = personaStore.getState().personas.find((p) => p.id === server.id); if (existingPersona) { - personaStore.getState().updatePersona(id, { modelPath }); - } else if (id !== 'base') { - personaStore.getState().addPersona({ id, label, description: label, colors: { torso: '#3b82f6', legs: '#3b82f6' }, modelPath }); + personaStore.getState().updatePersona(server.id, { modelPath: mergedModelPath }); + } else { + personaStore.getState().addPersona({ id: server.id, label: server.label, description: server.label, colors: { torso: '#3b82f6', legs: '#3b82f6' }, modelPath: mergedModelPath }); } } - }); + } + + // Add any local items that are missing from server + for (const local of localItems) { + if (!serverMap.has(local.id)) { + itemsToUpdate.push({ id: local.id, label: local.label, price: local.price, modelPath: local.modelPath }); + } + } + + // Push missing data back to server + if (itemsToUpdate.length > 0) { + for (const item of itemsToUpdate) { + fetch('/api/admin/pricing/', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }).catch(() => {}); + } + } + } else { + // DB is empty — push local items to server so they persist for all users + if (localItems.length > 0) { + await fetch('/api/admin/pricing/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ items: localItems.map((item, i) => ({ ...item, sortOrder: i })) }), + }); + } } } catch { // Fall back to localStorage data (already hydrated) diff --git a/src/store/usePersonaStore.ts b/src/store/usePersonaStore.ts index db84e4b..8426f0f 100644 --- a/src/store/usePersonaStore.ts +++ b/src/store/usePersonaStore.ts @@ -135,7 +135,7 @@ export const personaStore = createStore((set, get) => ({ set({ personas: [...DEFAULT_PERSONAS], isHydrated: true }); } - // Fetch pricing items from server DB and auto-register personas for items with a modelPath + // Fetch pricing items from server DB and auto-register personas for all attire items if (typeof window !== 'undefined') { fetch('/api/admin/pricing/') .then((r) => r.json()) @@ -144,13 +144,15 @@ export const personaStore = createStore((set, get) => ({ const current = get().personas; const currentIds = new Set(current.map((p) => p.id)); const newPersonas: PersonaOption[] = []; + // Items that should not appear as selectable personas + const excludeIds = new Set(['base', 'custom-color']); serverItems.forEach(({ id, label, modelPath }) => { - if (!modelPath || id === 'base') return; + if (excludeIds.has(id)) return; if (currentIds.has(id)) { // Update modelPath if it changed const existing = current.find((p) => p.id === id); - if (existing && existing.modelPath !== modelPath) { + if (existing && modelPath && existing.modelPath !== modelPath) { set((state) => ({ personas: state.personas.map((p) => p.id === id ? { ...p, modelPath } : p @@ -158,12 +160,13 @@ export const personaStore = createStore((set, get) => ({ })); } } else { + // Auto-create a persona entry for every pricing item newPersonas.push({ id, label, description: label, colors: { torso: '#3b82f6', legs: '#3b82f6' }, - modelPath, + ...(modelPath ? { modelPath } : {}), }); } }); diff --git a/src/store/usePricingStore.ts b/src/store/usePricingStore.ts index 86f5a8b..89b4292 100644 --- a/src/store/usePricingStore.ts +++ b/src/store/usePricingStore.ts @@ -125,8 +125,24 @@ export const pricingStore = createStore((set, get) => ({ .then((data) => { const serverItems: PricingItem[] = data.items ?? []; if (serverItems.length > 0) { - saveToStorage(serverItems); - set({ items: serverItems }); + // Merge: server wins for label/price, but keep local modelPath if server doesn't have one + const localItems = get().items; + const localMap = new Map(localItems.map((l) => [l.id, l])); + const merged = serverItems.map((s) => { + const local = localMap.get(s.id); + return { + ...s, + modelPath: s.modelPath || local?.modelPath, + }; + }); + // Also keep any local-only items not in server + for (const local of localItems) { + if (!serverItems.some((s) => s.id === local.id)) { + merged.push(local); + } + } + saveToStorage(merged); + set({ items: merged }); } }) .catch(() => {}); // silent — use local data as fallback