import { create } from 'zustand' import type { CableType, Catalog, DeviceTemplate, PortType } from '../domain/types' import type { CatalogRepository } from '../data/CatalogRepository' import { cableTypeToRow, deviceCategoryToRow, deviceTemplateToRow, manufacturerToRow, portTypeToRow } from '../data/catalogRowMapping' import { SupabaseCatalogRepository } from '../data/SupabaseCatalogRepository' import type { CatalogEntityType } from '../data/SubmissionRepository' import { useSubmissionStore } from './submissionStore' const repository: CatalogRepository = new SupabaseCatalogRepository() // Which public device templates you've hidden from your own palette — a // per-user UI preference, not catalog data, so it's not a Supabase row: hiding // a public entry only affects your view of it (see domain/diagram.ts's // allDeviceTemplates/hiddenDeviceTemplates), never the shared entry itself. const HIDDEN_PUBLIC_TEMPLATES_KEY = 'av-planner:hiddenPublicDeviceTemplateIds' function loadHiddenPublicIds(): Set { try { const raw = window.localStorage.getItem(HIDDEN_PUBLIC_TEMPLATES_KEY) return raw ? new Set(JSON.parse(raw)) : new Set() } catch { return new Set() } } function saveHiddenPublicIds(ids: Set): void { try { window.localStorage.setItem(HIDDEN_PUBLIC_TEMPLATES_KEY, JSON.stringify([...ids])) } catch { // Not essential — worst case a hidden template reappears next launch. } } const emptyCatalog: Catalog = { portTypes: [], cableTypes: [], deviceCategories: [], manufacturers: [], deviceTemplates: [] } interface CatalogStoreState { catalog: Catalog isLoaded: boolean hiddenPublicDeviceTemplateIds: Set loadCatalog: () => Promise addCustomDeviceCategory: (name: string) => Promise updateCustomDeviceCategory: (id: string, name: string) => Promise removeCustomDeviceCategory: (id: string) => Promise addCustomManufacturer: (name: string) => Promise updateCustomManufacturer: (id: string, name: string) => Promise removeCustomManufacturer: (id: string) => Promise addCustomPortType: (portType: Omit) => Promise updateCustomPortType: (id: string, patch: Partial>) => Promise /** Throws (like removeCustomDeviceCategory) if the delete is rejected — * most commonly a foreign-key violation because a device template's port * still references it. */ removeCustomPortType: (id: string) => Promise addCustomCableType: (cableType: Omit) => Promise updateCustomCableType: (id: string, patch: Partial>) => Promise removeCustomCableType: (id: string) => Promise addCustomDeviceTemplate: (template: Omit) => Promise updateCustomDeviceTemplate: (id: string, patch: Partial>) => Promise removeCustomDeviceTemplate: (id: string) => Promise hidePublicDeviceTemplate: (id: string) => void restorePublicDeviceTemplate: (id: string) => void /** Admin/Super-Admin direct-to-public creation — skips the submission/ * review workflow, for authoring the shared library yourself (see * CatalogRepository.adminAddDeviceCategory's own comment). */ adminAddDeviceCategory: (name: string) => Promise adminAddManufacturer: (name: string) => Promise adminAddPortType: (portType: Omit) => Promise adminAddCableType: (cableType: Omit) => Promise adminAddDeviceTemplate: (template: Omit) => Promise /** Admin/Super-Admin direct edit of an already-public entry, per * organized-ideas.md §6 — bypasses the submission/review workflow * entirely (RLS's is_admin() clause is what actually permits it). */ adminUpdatePortType: (id: string, patch: Partial>) => Promise adminUpdateCableType: (id: string, patch: Partial>) => Promise adminUpdateDeviceTemplate: (id: string, patch: Partial>) => Promise adminUpdateDeviceCategory: (id: string, name: string) => Promise adminUpdateManufacturer: (id: string, name: string) => Promise /** Unpublishes (is_public -> false) rather than deletes — see * CatalogRepository.adminUnpublish. */ adminUnpublish: (entityType: CatalogEntityType, id: string) => Promise /** Permanent delete of a built-in entry — see * CatalogRepository.adminRemoveDeviceCategory's own comment for the * zero-usage contract the caller is expected to have already checked. */ adminRemoveDeviceCategory: (id: string) => Promise adminRemoveManufacturer: (id: string) => Promise adminRemovePortType: (id: string) => Promise adminRemoveCableType: (id: string) => Promise adminRemoveDeviceTemplate: (id: string) => Promise } export const useCatalogStore = create((set, get) => ({ catalog: emptyCatalog, isLoaded: false, hiddenPublicDeviceTemplateIds: loadHiddenPublicIds(), loadCatalog: async () => { const catalog = await repository.load() set({ catalog, isLoaded: true }) }, addCustomDeviceCategory: async (name) => { const category = await repository.addDeviceCategory(name) set((state) => ({ catalog: { ...state.catalog, deviceCategories: [...state.catalog.deviceCategories, category] } })) return category.id }, updateCustomDeviceCategory: async (id, name) => { await repository.updateDeviceCategory(id, name) set((state) => ({ catalog: { ...state.catalog, deviceCategories: state.catalog.deviceCategories.map((c) => (c.id === id ? { ...c, name } : c)), }, })) useSubmissionStore.getState().syncProposedData('device_category', id, deviceCategoryToRow(name)) }, removeCustomDeviceCategory: async (id) => { await repository.removeDeviceCategory(id) set((state) => ({ catalog: { ...state.catalog, deviceCategories: state.catalog.deviceCategories.filter((c) => c.id !== id) }, })) }, adminAddDeviceCategory: async (name) => { const category = await repository.adminAddDeviceCategory(name) set((state) => ({ catalog: { ...state.catalog, deviceCategories: [...state.catalog.deviceCategories, category] } })) return category.id }, addCustomManufacturer: async (name) => { const manufacturer = await repository.addManufacturer(name) set((state) => ({ catalog: { ...state.catalog, manufacturers: [...state.catalog.manufacturers, manufacturer] } })) return manufacturer.id }, updateCustomManufacturer: async (id, name) => { await repository.updateManufacturer(id, name) set((state) => ({ catalog: { ...state.catalog, manufacturers: state.catalog.manufacturers.map((m) => (m.id === id ? { ...m, name } : m)), }, })) useSubmissionStore.getState().syncProposedData('manufacturer', id, manufacturerToRow(name)) }, removeCustomManufacturer: async (id) => { await repository.removeManufacturer(id) set((state) => ({ catalog: { ...state.catalog, manufacturers: state.catalog.manufacturers.filter((m) => m.id !== id) }, })) }, adminAddManufacturer: async (name) => { const manufacturer = await repository.adminAddManufacturer(name) set((state) => ({ catalog: { ...state.catalog, manufacturers: [...state.catalog.manufacturers, manufacturer] } })) return manufacturer.id }, addCustomPortType: async (portType) => { const created = await repository.addPortType(portType) set((state) => ({ catalog: { ...state.catalog, portTypes: [...state.catalog.portTypes, created] } })) }, adminAddPortType: async (portType) => { const created = await repository.adminAddPortType(portType) set((state) => ({ catalog: { ...state.catalog, portTypes: [...state.catalog.portTypes, created] } })) }, updateCustomPortType: async (id, patch) => { await repository.updatePortType(id, patch) set((state) => ({ catalog: { ...state.catalog, portTypes: state.catalog.portTypes.map((pt) => (pt.id === id ? { ...pt, ...patch } : pt)), }, })) const updated = get().catalog.portTypes.find((pt) => pt.id === id) if (updated) useSubmissionStore.getState().syncProposedData('port_type', id, portTypeToRow(updated)) }, removeCustomPortType: async (id) => { await repository.removePortType(id) set((state) => ({ catalog: { ...state.catalog, portTypes: state.catalog.portTypes.filter((pt) => pt.id !== id) } })) }, addCustomCableType: async (cableType) => { const created = await repository.addCableType(cableType) set((state) => ({ catalog: { ...state.catalog, cableTypes: [...state.catalog.cableTypes, created] } })) }, adminAddCableType: async (cableType) => { const created = await repository.adminAddCableType(cableType) set((state) => ({ catalog: { ...state.catalog, cableTypes: [...state.catalog.cableTypes, created] } })) }, updateCustomCableType: async (id, patch) => { await repository.updateCableType(id, patch) set((state) => ({ catalog: { ...state.catalog, cableTypes: state.catalog.cableTypes.map((ct) => (ct.id === id ? { ...ct, ...patch } : ct)), }, })) const updated = get().catalog.cableTypes.find((ct) => ct.id === id) if (updated) useSubmissionStore.getState().syncProposedData('cable_type', id, cableTypeToRow(updated)) }, removeCustomCableType: async (id) => { await repository.removeCableType(id) set((state) => ({ catalog: { ...state.catalog, cableTypes: state.catalog.cableTypes.filter((ct) => ct.id !== id) } })) }, addCustomDeviceTemplate: async (template) => { const created = await repository.addDeviceTemplate(template) set((state) => ({ catalog: { ...state.catalog, deviceTemplates: [...state.catalog.deviceTemplates, created] } })) }, adminAddDeviceTemplate: async (template) => { const created = await repository.adminAddDeviceTemplate(template) set((state) => ({ catalog: { ...state.catalog, deviceTemplates: [...state.catalog.deviceTemplates, created] } })) }, updateCustomDeviceTemplate: async (id, patch) => { await repository.updateDeviceTemplate(id, patch) set((state) => ({ catalog: { ...state.catalog, deviceTemplates: state.catalog.deviceTemplates.map((t) => (t.id === id ? { ...t, ...patch } : t)), }, })) const updated = get().catalog.deviceTemplates.find((t) => t.id === id) if (updated) useSubmissionStore.getState().syncProposedData('device_template', id, deviceTemplateToRow(updated)) }, removeCustomDeviceTemplate: async (id) => { await repository.removeDeviceTemplate(id) set((state) => ({ catalog: { ...state.catalog, deviceTemplates: state.catalog.deviceTemplates.filter((t) => t.id !== id) }, })) }, hidePublicDeviceTemplate: (id) => { const next = new Set(get().hiddenPublicDeviceTemplateIds) next.add(id) saveHiddenPublicIds(next) set({ hiddenPublicDeviceTemplateIds: next }) }, restorePublicDeviceTemplate: (id) => { const next = new Set(get().hiddenPublicDeviceTemplateIds) next.delete(id) saveHiddenPublicIds(next) set({ hiddenPublicDeviceTemplateIds: next }) }, // Re-fetch the whole catalog after each of these rather than patching // locally — device_template edits also touch device_template_ports, and // unpublish changes which rows even show up (custom flips true for the // Admin who did it, since is_admin() is the only thing still granting // them visibility — see adminUnpublish's own comment). Simplest correct // thing, and Admin actions here are infrequent enough that the extra // round trip doesn't matter. adminUpdatePortType: async (id, patch) => { await repository.adminUpdatePortType(id, patch) await get().loadCatalog() }, adminUpdateCableType: async (id, patch) => { await repository.adminUpdateCableType(id, patch) await get().loadCatalog() }, adminUpdateDeviceTemplate: async (id, patch) => { await repository.adminUpdateDeviceTemplate(id, patch) await get().loadCatalog() }, adminUpdateDeviceCategory: async (id, name) => { await repository.adminUpdateDeviceCategory(id, name) await get().loadCatalog() }, adminUpdateManufacturer: async (id, name) => { await repository.adminUpdateManufacturer(id, name) await get().loadCatalog() }, adminUnpublish: async (entityType, id) => { await repository.adminUnpublish(entityType, id) await get().loadCatalog() }, adminRemoveDeviceCategory: async (id) => { await repository.adminRemoveDeviceCategory(id) set((state) => ({ catalog: { ...state.catalog, deviceCategories: state.catalog.deviceCategories.filter((c) => c.id !== id) }, })) }, adminRemoveManufacturer: async (id) => { await repository.adminRemoveManufacturer(id) set((state) => ({ catalog: { ...state.catalog, manufacturers: state.catalog.manufacturers.filter((m) => m.id !== id) }, })) }, adminRemovePortType: async (id) => { await repository.adminRemovePortType(id) set((state) => ({ catalog: { ...state.catalog, portTypes: state.catalog.portTypes.filter((pt) => pt.id !== id) } })) }, adminRemoveCableType: async (id) => { await repository.adminRemoveCableType(id) set((state) => ({ catalog: { ...state.catalog, cableTypes: state.catalog.cableTypes.filter((ct) => ct.id !== id) } })) }, adminRemoveDeviceTemplate: async (id) => { await repository.adminRemoveDeviceTemplate(id) set((state) => ({ catalog: { ...state.catalog, deviceTemplates: state.catalog.deviceTemplates.filter((t) => t.id !== id) }, })) }, }))