diff --git a/src/core/tagging/tag-service.ts b/src/core/tagging/tag-service.ts index d955599..d85e185 100644 --- a/src/core/tagging/tag-service.ts +++ b/src/core/tagging/tag-service.ts @@ -1,40 +1,45 @@ -import type { StorageService } from '@/core/storage/types'; -import type { Tag, TagCategory, TagScope } from './types'; -import { v4 as uuid } from 'uuid'; +import type { StorageService } from "@/core/storage/types"; +import type { Tag, TagCategory, TagScope } from "./types"; +import { v4 as uuid } from "uuid"; -const NAMESPACE = 'tags'; +const NAMESPACE = "tags"; export class TagService { constructor(private storage: StorageService) {} async getAllTags(): Promise { - const keys = await this.storage.list(NAMESPACE); + const all = await this.storage.export(NAMESPACE); const tags: Tag[] = []; - for (const key of keys) { - const tag = await this.storage.get(NAMESPACE, key); - if (tag) tags.push(tag); + for (const value of Object.values(all)) { + if (value) tags.push(value as Tag); } return tags; } async getTagsByCategory(category: TagCategory): Promise { - return this.storage.query(NAMESPACE, (tag) => tag.category === category); + return this.storage.query( + NAMESPACE, + (tag) => tag.category === category, + ); } async getTagsByScope(scope: TagScope, scopeId?: string): Promise { return this.storage.query(NAMESPACE, (tag) => { if (tag.scope !== scope) return false; - if (scope === 'module' && scopeId) return tag.moduleId === scopeId; - if (scope === 'company' && scopeId) return tag.companyId === scopeId; + if (scope === "module" && scopeId) return tag.moduleId === scopeId; + if (scope === "company" && scopeId) return tag.companyId === scopeId; return true; }); } async getChildren(parentId: string): Promise { - return this.storage.query(NAMESPACE, (tag) => tag.parentId === parentId); + return this.storage.query( + NAMESPACE, + (tag) => tag.parentId === parentId, + ); } - async createTag(data: Omit): Promise { + async createTag(data: Omit): Promise { const tag: Tag = { ...data, id: uuid(), @@ -44,10 +49,17 @@ export class TagService { return tag; } - async updateTag(id: string, updates: Partial>): Promise { + async updateTag( + id: string, + updates: Partial>, + ): Promise { const existing = await this.storage.get(NAMESPACE, id); if (!existing) return null; - const updated: Tag = { ...existing, ...updates, updatedAt: new Date().toISOString() }; + const updated: Tag = { + ...existing, + ...updates, + updatedAt: new Date().toISOString(), + }; await this.storage.set(NAMESPACE, id, updated); return updated; } @@ -63,16 +75,20 @@ export class TagService { async searchTags(query: string): Promise { const lower = query.toLowerCase(); - return this.storage.query(NAMESPACE, (tag) => - tag.label.toLowerCase().includes(lower) || - (tag.projectCode?.toLowerCase().includes(lower) ?? false) + return this.storage.query( + NAMESPACE, + (tag) => + tag.label.toLowerCase().includes(lower) || + (tag.projectCode?.toLowerCase().includes(lower) ?? false), ); } /** Bulk import tags (for seed data). Skips tags whose label already exists in same category. */ - async importTags(tags: Omit[]): Promise { + async importTags(tags: Omit[]): Promise { const existing = await this.getAllTags(); - const existingKeys = new Set(existing.map((t) => `${t.category}::${t.label}`)); + const existingKeys = new Set( + existing.map((t) => `${t.category}::${t.label}`), + ); let imported = 0; for (const data of tags) { const key = `${data.category}::${data.label}`; diff --git a/src/modules/address-book/hooks/use-contacts.ts b/src/modules/address-book/hooks/use-contacts.ts index 6c2695e..2816fdc 100644 --- a/src/modules/address-book/hooks/use-contacts.ts +++ b/src/modules/address-book/hooks/use-contacts.ts @@ -1,31 +1,33 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { AddressContact, ContactType } from '../types'; +import { useState, useEffect, useCallback } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { AddressContact, ContactType } from "../types"; -const PREFIX = 'contact:'; +const PREFIX = "contact:"; export interface ContactFilters { search: string; - type: ContactType | 'all'; + type: ContactType | "all"; } export function useContacts() { - const storage = useStorage('address-book'); + const storage = useStorage("address-book"); const [contacts, setContacts] = useState([]); const [loading, setLoading] = useState(true); - const [filters, setFilters] = useState({ search: '', type: 'all' }); + const [filters, setFilters] = useState({ + search: "", + type: "all", + }); const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: AddressContact[] = []; - for (const key of keys) { - if (key.startsWith(PREFIX)) { - const item = await storage.get(key); - if (item) results.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(PREFIX) && value) { + results.push(value as AddressContact); } } results.sort((a, b) => a.name.localeCompare(b.name)); @@ -34,41 +36,60 @@ export function useContacts() { }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { + refresh(); + }, [refresh]); - const addContact = useCallback(async (data: Omit) => { - const now = new Date().toISOString(); - const contact: AddressContact = { ...data, id: uuid(), createdAt: now, updatedAt: now }; - await storage.set(`${PREFIX}${contact.id}`, contact); - await refresh(); - return contact; - }, [storage, refresh]); + const addContact = useCallback( + async (data: Omit) => { + const now = new Date().toISOString(); + const contact: AddressContact = { + ...data, + id: uuid(), + createdAt: now, + updatedAt: now, + }; + await storage.set(`${PREFIX}${contact.id}`, contact); + await refresh(); + return contact; + }, + [storage, refresh], + ); - const updateContact = useCallback(async (id: string, updates: Partial) => { - const existing = contacts.find((c) => c.id === id); - if (!existing) return; - const updated: AddressContact = { - ...existing, - ...updates, - id: existing.id, - createdAt: existing.createdAt, - updatedAt: new Date().toISOString(), - }; - await storage.set(`${PREFIX}${id}`, updated); - await refresh(); - }, [storage, refresh, contacts]); + const updateContact = useCallback( + async (id: string, updates: Partial) => { + const existing = contacts.find((c) => c.id === id); + if (!existing) return; + const updated: AddressContact = { + ...existing, + ...updates, + id: existing.id, + createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; + await storage.set(`${PREFIX}${id}`, updated); + await refresh(); + }, + [storage, refresh, contacts], + ); - const removeContact = useCallback(async (id: string) => { - await storage.delete(`${PREFIX}${id}`); - await refresh(); - }, [storage, refresh]); + const removeContact = useCallback( + async (id: string) => { + await storage.delete(`${PREFIX}${id}`); + await refresh(); + }, + [storage, refresh], + ); - const updateFilter = useCallback((key: K, value: ContactFilters[K]) => { - setFilters((prev) => ({ ...prev, [key]: value })); - }, []); + const updateFilter = useCallback( + (key: K, value: ContactFilters[K]) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, + [], + ); const filteredContacts = contacts.filter((c) => { - if (filters.type !== 'all' && c.type !== filters.type) return false; + if (filters.type !== "all" && c.type !== filters.type) return false; if (filters.search) { const q = filters.search.toLowerCase(); return ( @@ -76,12 +97,22 @@ export function useContacts() { c.company.toLowerCase().includes(q) || c.email.toLowerCase().includes(q) || c.phone.includes(q) || - (c.department ?? '').toLowerCase().includes(q) || - (c.role ?? '').toLowerCase().includes(q) + (c.department ?? "").toLowerCase().includes(q) || + (c.role ?? "").toLowerCase().includes(q) ); } return true; }); - return { contacts: filteredContacts, allContacts: contacts, loading, filters, updateFilter, addContact, updateContact, removeContact, refresh }; + return { + contacts: filteredContacts, + allContacts: contacts, + loading, + filters, + updateFilter, + addContact, + updateContact, + removeContact, + refresh, + }; } diff --git a/src/modules/ai-chat/hooks/use-chat.ts b/src/modules/ai-chat/hooks/use-chat.ts index fd517ac..d495f5d 100644 --- a/src/modules/ai-chat/hooks/use-chat.ts +++ b/src/modules/ai-chat/hooks/use-chat.ts @@ -1,14 +1,14 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { ChatMessage, ChatSession } from '../types'; +import { useState, useEffect, useCallback } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { ChatMessage, ChatSession } from "../types"; -const SESSION_PREFIX = 'session:'; +const SESSION_PREFIX = "session:"; export function useChat() { - const storage = useStorage('ai-chat'); + const storage = useStorage("ai-chat"); const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [loading, setLoading] = useState(true); @@ -17,12 +17,11 @@ export function useChat() { const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: ChatSession[] = []; - for (const key of keys) { - if (key.startsWith(SESSION_PREFIX)) { - const session = await storage.get(key); - if (session) results.push(session); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(SESSION_PREFIX) && value) { + results.push(value as ChatSession); } } results.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); @@ -31,49 +30,65 @@ export function useChat() { }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { + refresh(); + }, [refresh]); - const createSession = useCallback(async (title?: string) => { - const session: ChatSession = { - id: uuid(), - title: title || `Conversație ${new Date().toLocaleDateString('ro-RO')}`, - messages: [], - createdAt: new Date().toISOString(), - }; - await storage.set(`${SESSION_PREFIX}${session.id}`, session); - setSessions((prev) => [session, ...prev]); - setActiveSessionId(session.id); - return session; - }, [storage]); + const createSession = useCallback( + async (title?: string) => { + const session: ChatSession = { + id: uuid(), + title: title || `Conversație ${new Date().toLocaleDateString("ro-RO")}`, + messages: [], + createdAt: new Date().toISOString(), + }; + await storage.set(`${SESSION_PREFIX}${session.id}`, session); + setSessions((prev) => [session, ...prev]); + setActiveSessionId(session.id); + return session; + }, + [storage], + ); - const addMessage = useCallback(async (content: string, role: ChatMessage['role']) => { - if (!activeSessionId) return; - const message: ChatMessage = { - id: uuid(), - role, - content, - timestamp: new Date().toISOString(), - }; - setSessions((prev) => prev.map((s) => { - if (s.id !== activeSessionId) return s; - return { ...s, messages: [...s.messages, message] }; - })); - // Persist - const current = sessions.find((s) => s.id === activeSessionId); - if (current) { - const updated = { ...current, messages: [...current.messages, message] }; - await storage.set(`${SESSION_PREFIX}${activeSessionId}`, updated); - } - return message; - }, [storage, activeSessionId, sessions]); + const addMessage = useCallback( + async (content: string, role: ChatMessage["role"]) => { + if (!activeSessionId) return; + const message: ChatMessage = { + id: uuid(), + role, + content, + timestamp: new Date().toISOString(), + }; + setSessions((prev) => + prev.map((s) => { + if (s.id !== activeSessionId) return s; + return { ...s, messages: [...s.messages, message] }; + }), + ); + // Persist + const current = sessions.find((s) => s.id === activeSessionId); + if (current) { + const updated = { + ...current, + messages: [...current.messages, message], + }; + await storage.set(`${SESSION_PREFIX}${activeSessionId}`, updated); + } + return message; + }, + [storage, activeSessionId, sessions], + ); - const deleteSession = useCallback(async (id: string) => { - await storage.delete(`${SESSION_PREFIX}${id}`); - setSessions((prev) => prev.filter((s) => s.id !== id)); - if (activeSessionId === id) { - setActiveSessionId(null); - } - }, [storage, activeSessionId]); + const deleteSession = useCallback( + async (id: string) => { + await storage.delete(`${SESSION_PREFIX}${id}`); + setSessions((prev) => prev.filter((s) => s.id !== id)); + if (activeSessionId === id) { + setActiveSessionId(null); + } + }, + [storage, activeSessionId], + ); const selectSession = useCallback((id: string) => { setActiveSessionId(id); diff --git a/src/modules/digital-signatures/hooks/use-signatures.ts b/src/modules/digital-signatures/hooks/use-signatures.ts index 5a721fb..2e1353b 100644 --- a/src/modules/digital-signatures/hooks/use-signatures.ts +++ b/src/modules/digital-signatures/hooks/use-signatures.ts @@ -1,31 +1,37 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { SignatureAsset, SignatureAssetType, AssetVersion } from '../types'; +import { useState, useEffect, useCallback } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { + SignatureAsset, + SignatureAssetType, + AssetVersion, +} from "../types"; -const PREFIX = 'sig:'; +const PREFIX = "sig:"; export interface SignatureFilters { search: string; - type: SignatureAssetType | 'all'; + type: SignatureAssetType | "all"; } export function useSignatures() { - const storage = useStorage('digital-signatures'); + const storage = useStorage("digital-signatures"); const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(true); - const [filters, setFilters] = useState({ search: '', type: 'all' }); + const [filters, setFilters] = useState({ + search: "", + type: "all", + }); const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: SignatureAsset[] = []; - for (const key of keys) { - if (key.startsWith(PREFIX)) { - const item = await storage.get(key); - if (item) results.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(PREFIX) && value) { + results.push(value as SignatureAsset); } } results.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); @@ -34,53 +40,95 @@ export function useSignatures() { }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { + refresh(); + }, [refresh]); - const addAsset = useCallback(async (data: Omit) => { - const now = new Date().toISOString(); - const asset: SignatureAsset = { ...data, id: uuid(), createdAt: now, updatedAt: now }; - await storage.set(`${PREFIX}${asset.id}`, asset); - await refresh(); - return asset; - }, [storage, refresh]); + const addAsset = useCallback( + async (data: Omit) => { + const now = new Date().toISOString(); + const asset: SignatureAsset = { + ...data, + id: uuid(), + createdAt: now, + updatedAt: now, + }; + await storage.set(`${PREFIX}${asset.id}`, asset); + await refresh(); + return asset; + }, + [storage, refresh], + ); - const updateAsset = useCallback(async (id: string, updates: Partial) => { - const existing = assets.find((a) => a.id === id); - if (!existing) return; - const updated: SignatureAsset = { - ...existing, ...updates, - id: existing.id, createdAt: existing.createdAt, - updatedAt: new Date().toISOString(), - }; - await storage.set(`${PREFIX}${id}`, updated); - await refresh(); - }, [storage, refresh, assets]); + const updateAsset = useCallback( + async (id: string, updates: Partial) => { + const existing = assets.find((a) => a.id === id); + if (!existing) return; + const updated: SignatureAsset = { + ...existing, + ...updates, + id: existing.id, + createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; + await storage.set(`${PREFIX}${id}`, updated); + await refresh(); + }, + [storage, refresh, assets], + ); - const addVersion = useCallback(async (assetId: string, imageUrl: string, notes: string) => { - const existing = assets.find((a) => a.id === assetId); - if (!existing) return; - const version: AssetVersion = { id: uuid(), imageUrl, notes, createdAt: new Date().toISOString() }; - const updatedVersions = [...(existing.versions ?? []), version]; - await updateAsset(assetId, { imageUrl, versions: updatedVersions }); - }, [assets, updateAsset]); + const addVersion = useCallback( + async (assetId: string, imageUrl: string, notes: string) => { + const existing = assets.find((a) => a.id === assetId); + if (!existing) return; + const version: AssetVersion = { + id: uuid(), + imageUrl, + notes, + createdAt: new Date().toISOString(), + }; + const updatedVersions = [...(existing.versions ?? []), version]; + await updateAsset(assetId, { imageUrl, versions: updatedVersions }); + }, + [assets, updateAsset], + ); - const removeAsset = useCallback(async (id: string) => { - await storage.delete(`${PREFIX}${id}`); - await refresh(); - }, [storage, refresh]); + const removeAsset = useCallback( + async (id: string) => { + await storage.delete(`${PREFIX}${id}`); + await refresh(); + }, + [storage, refresh], + ); - const updateFilter = useCallback((key: K, value: SignatureFilters[K]) => { - setFilters((prev) => ({ ...prev, [key]: value })); - }, []); + const updateFilter = useCallback( + (key: K, value: SignatureFilters[K]) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, + [], + ); const filteredAssets = assets.filter((a) => { - if (filters.type !== 'all' && a.type !== filters.type) return false; + if (filters.type !== "all" && a.type !== filters.type) return false; if (filters.search) { const q = filters.search.toLowerCase(); - return a.label.toLowerCase().includes(q) || a.owner.toLowerCase().includes(q); + return ( + a.label.toLowerCase().includes(q) || a.owner.toLowerCase().includes(q) + ); } return true; }); - return { assets: filteredAssets, allAssets: assets, loading, filters, updateFilter, addAsset, updateAsset, addVersion, removeAsset, refresh }; + return { + assets: filteredAssets, + allAssets: assets, + loading, + filters, + updateFilter, + addAsset, + updateAsset, + addVersion, + removeAsset, + refresh, + }; } diff --git a/src/modules/email-signature/hooks/use-saved-signatures.ts b/src/modules/email-signature/hooks/use-saved-signatures.ts index b79b0d9..151053c 100644 --- a/src/modules/email-signature/hooks/use-saved-signatures.ts +++ b/src/modules/email-signature/hooks/use-saved-signatures.ts @@ -1,23 +1,22 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { SignatureConfig, SavedSignature } from '../types'; +import { useState, useEffect, useCallback } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { SignatureConfig, SavedSignature } from "../types"; export function useSavedSignatures() { - const storage = useStorage('email-signature'); + const storage = useStorage("email-signature"); const [saved, setSaved] = useState([]); const [loading, setLoading] = useState(true); const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const items: SavedSignature[] = []; - for (const key of keys) { - if (key.startsWith('sig:')) { - const item = await storage.get(key); - if (item) items.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith("sig:") && value) { + items.push(value as SavedSignature); } } items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); @@ -26,26 +25,34 @@ export function useSavedSignatures() { }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { + refresh(); + }, [refresh]); - const save = useCallback(async (label: string, config: SignatureConfig) => { - const now = new Date().toISOString(); - const entry: SavedSignature = { - id: uuid(), - label, - config, - createdAt: now, - updatedAt: now, - }; - await storage.set(`sig:${entry.id}`, entry); - await refresh(); - return entry; - }, [storage, refresh]); + const save = useCallback( + async (label: string, config: SignatureConfig) => { + const now = new Date().toISOString(); + const entry: SavedSignature = { + id: uuid(), + label, + config, + createdAt: now, + updatedAt: now, + }; + await storage.set(`sig:${entry.id}`, entry); + await refresh(); + return entry; + }, + [storage, refresh], + ); - const remove = useCallback(async (id: string) => { - await storage.delete(`sig:${id}`); - await refresh(); - }, [storage, refresh]); + const remove = useCallback( + async (id: string) => { + await storage.delete(`sig:${id}`); + await refresh(); + }, + [storage, refresh], + ); return { saved, loading, save, remove, refresh }; } diff --git a/src/modules/hot-desk/hooks/use-reservations.ts b/src/modules/hot-desk/hooks/use-reservations.ts index 954d006..d915598 100644 --- a/src/modules/hot-desk/hooks/use-reservations.ts +++ b/src/modules/hot-desk/hooks/use-reservations.ts @@ -14,12 +14,11 @@ export function useReservations() { const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: DeskReservation[] = []; - for (const key of keys) { - if (key.startsWith(PREFIX)) { - const item = await storage.get(key); - if (item) results.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(PREFIX) && value) { + results.push(value as DeskReservation); } } results.sort((a, b) => a.date.localeCompare(b.date)); diff --git a/src/modules/it-inventory/components/server-rack.tsx b/src/modules/it-inventory/components/server-rack.tsx index 8f14704..b5e055b 100644 --- a/src/modules/it-inventory/components/server-rack.tsx +++ b/src/modules/it-inventory/components/server-rack.tsx @@ -68,6 +68,9 @@ export function ServerRack({ items }: ServerRackProps) { return result; }, [items]); + // Reverse so U1 is at the bottom (standard rack numbering) + const displaySlots = useMemo(() => [...slots].reverse(), [slots]); + const rackItems = items.filter((i) => i.rackPosition); if (rackItems.length === 0) { @@ -97,7 +100,7 @@ export function ServerRack({ items }: ServerRackProps) { {/* Units */}
- {slots.map((slot) => { + {displaySlots.map((slot) => { if (!slot.isStart && slot.item) return null; const heightPx = slot.height * 24; diff --git a/src/modules/it-inventory/hooks/use-inventory.ts b/src/modules/it-inventory/hooks/use-inventory.ts index fdd2d0b..13be6a1 100644 --- a/src/modules/it-inventory/hooks/use-inventory.ts +++ b/src/modules/it-inventory/hooks/use-inventory.ts @@ -27,12 +27,11 @@ export function useInventory() { const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: InventoryItem[] = []; - for (const key of keys) { - if (key.startsWith(PREFIX)) { - const item = await storage.get(key); - if (item) results.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(PREFIX) && value) { + results.push(value as InventoryItem); } } results.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); diff --git a/src/modules/password-vault/hooks/use-vault.ts b/src/modules/password-vault/hooks/use-vault.ts index 90550a6..6dc8c1b 100644 --- a/src/modules/password-vault/hooks/use-vault.ts +++ b/src/modules/password-vault/hooks/use-vault.ts @@ -1,31 +1,33 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { VaultEntry, VaultEntryCategory } from '../types'; +import { useState, useEffect, useCallback } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { VaultEntry, VaultEntryCategory } from "../types"; -const PREFIX = 'vault:'; +const PREFIX = "vault:"; export interface VaultFilters { search: string; - category: VaultEntryCategory | 'all'; + category: VaultEntryCategory | "all"; } export function useVault() { - const storage = useStorage('password-vault'); + const storage = useStorage("password-vault"); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); - const [filters, setFilters] = useState({ search: '', category: 'all' }); + const [filters, setFilters] = useState({ + search: "", + category: "all", + }); const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: VaultEntry[] = []; - for (const key of keys) { - if (key.startsWith(PREFIX)) { - const item = await storage.get(key); - if (item) results.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(PREFIX) && value) { + results.push(value as VaultEntry); } } results.sort((a, b) => a.label.localeCompare(b.label)); @@ -34,41 +36,81 @@ export function useVault() { }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { + refresh(); + }, [refresh]); - const addEntry = useCallback(async (data: Omit) => { - const now = new Date().toISOString(); - const entry: VaultEntry = { ...data, id: uuid(), createdAt: now, updatedAt: now }; - await storage.set(`${PREFIX}${entry.id}`, entry); - await refresh(); - return entry; - }, [storage, refresh]); + const addEntry = useCallback( + async (data: Omit) => { + const now = new Date().toISOString(); + const entry: VaultEntry = { + ...data, + id: uuid(), + createdAt: now, + updatedAt: now, + }; + await storage.set(`${PREFIX}${entry.id}`, entry); + await refresh(); + return entry; + }, + [storage, refresh], + ); - const updateEntry = useCallback(async (id: string, updates: Partial) => { - const existing = entries.find((e) => e.id === id); - if (!existing) return; - const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt, updatedAt: new Date().toISOString() }; - await storage.set(`${PREFIX}${id}`, updated); - await refresh(); - }, [storage, refresh, entries]); + const updateEntry = useCallback( + async (id: string, updates: Partial) => { + const existing = entries.find((e) => e.id === id); + if (!existing) return; + const updated = { + ...existing, + ...updates, + id: existing.id, + createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; + await storage.set(`${PREFIX}${id}`, updated); + await refresh(); + }, + [storage, refresh, entries], + ); - const removeEntry = useCallback(async (id: string) => { - await storage.delete(`${PREFIX}${id}`); - await refresh(); - }, [storage, refresh]); + const removeEntry = useCallback( + async (id: string) => { + await storage.delete(`${PREFIX}${id}`); + await refresh(); + }, + [storage, refresh], + ); - const updateFilter = useCallback((key: K, value: VaultFilters[K]) => { - setFilters((prev) => ({ ...prev, [key]: value })); - }, []); + const updateFilter = useCallback( + (key: K, value: VaultFilters[K]) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, + [], + ); const filteredEntries = entries.filter((e) => { - if (filters.category !== 'all' && e.category !== filters.category) return false; + if (filters.category !== "all" && e.category !== filters.category) + return false; if (filters.search) { const q = filters.search.toLowerCase(); - return e.label.toLowerCase().includes(q) || e.username.toLowerCase().includes(q) || e.url.toLowerCase().includes(q); + return ( + e.label.toLowerCase().includes(q) || + e.username.toLowerCase().includes(q) || + e.url.toLowerCase().includes(q) + ); } return true; }); - return { entries: filteredEntries, allEntries: entries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry, refresh }; + return { + entries: filteredEntries, + allEntries: entries, + loading, + filters, + updateFilter, + addEntry, + updateEntry, + removeEntry, + refresh, + }; } diff --git a/src/modules/prompt-generator/hooks/use-prompt-generator.ts b/src/modules/prompt-generator/hooks/use-prompt-generator.ts index 04760a5..e34354a 100644 --- a/src/modules/prompt-generator/hooks/use-prompt-generator.ts +++ b/src/modules/prompt-generator/hooks/use-prompt-generator.ts @@ -1,39 +1,42 @@ -'use client'; +"use client"; -import { useState, useCallback, useMemo, useEffect } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { PromptTemplate, PromptHistoryEntry, OutputMode } from '../types'; -import { BUILTIN_TEMPLATES } from '../services/builtin-templates'; -import { composePrompt } from '../services/prompt-composer'; +import { useState, useCallback, useMemo, useEffect } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { PromptTemplate, PromptHistoryEntry, OutputMode } from "../types"; +import { BUILTIN_TEMPLATES } from "../services/builtin-templates"; +import { composePrompt } from "../services/prompt-composer"; -const HISTORY_PREFIX = 'history:'; -const TEMPLATE_PREFIX = 'template:'; +const HISTORY_PREFIX = "history:"; +const TEMPLATE_PREFIX = "template:"; export function usePromptGenerator() { - const storage = useStorage('prompt-generator'); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const storage = useStorage("prompt-generator"); + const [selectedTemplate, setSelectedTemplate] = + useState(null); const [values, setValues] = useState>({}); const [customTemplates, setCustomTemplates] = useState([]); const [history, setHistory] = useState([]); const [loading, setLoading] = useState(true); - const allTemplates = useMemo(() => [...BUILTIN_TEMPLATES, ...customTemplates], [customTemplates]); + const allTemplates = useMemo( + () => [...BUILTIN_TEMPLATES, ...customTemplates], + [customTemplates], + ); // Load custom templates and history useEffect(() => { async function load() { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const templates: PromptTemplate[] = []; const entries: PromptHistoryEntry[] = []; - for (const key of keys) { + for (const [key, value] of Object.entries(all)) { + if (!value) continue; if (key.startsWith(TEMPLATE_PREFIX)) { - const t = await storage.get(key); - if (t) templates.push(t); + templates.push(value as PromptTemplate); } else if (key.startsWith(HISTORY_PREFIX)) { - const h = await storage.get(key); - if (h) entries.push(h); + entries.push(value as PromptHistoryEntry); } } entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); @@ -59,7 +62,7 @@ export function usePromptGenerator() { }, []); const composedPrompt = useMemo(() => { - if (!selectedTemplate) return ''; + if (!selectedTemplate) return ""; return composePrompt(selectedTemplate, values); }, [selectedTemplate, values]); @@ -74,7 +77,10 @@ export function usePromptGenerator() { composedPrompt, outputMode: selectedTemplate.outputMode, providerProfile: selectedTemplate.providerProfile ?? null, - safetyBlocks: selectedTemplate.safetyBlocks?.filter((s) => s.enabled).map((s) => s.id) ?? [], + safetyBlocks: + selectedTemplate.safetyBlocks + ?.filter((s) => s.enabled) + .map((s) => s.id) ?? [], tags: selectedTemplate.tags, isFavorite: false, createdAt: new Date().toISOString(), @@ -84,10 +90,13 @@ export function usePromptGenerator() { return entry; }, [storage, selectedTemplate, values, composedPrompt]); - const deleteHistoryEntry = useCallback(async (id: string) => { - await storage.delete(`${HISTORY_PREFIX}${id}`); - setHistory((prev) => prev.filter((h) => h.id !== id)); - }, [storage]); + const deleteHistoryEntry = useCallback( + async (id: string) => { + await storage.delete(`${HISTORY_PREFIX}${id}`); + setHistory((prev) => prev.filter((h) => h.id !== id)); + }, + [storage], + ); const clearSelection = useCallback(() => { setSelectedTemplate(null); diff --git a/src/modules/registratura/hooks/use-registry.ts b/src/modules/registratura/hooks/use-registry.ts index 3c0ad38..89d1bf3 100644 --- a/src/modules/registratura/hooks/use-registry.ts +++ b/src/modules/registratura/hooks/use-registry.ts @@ -59,7 +59,8 @@ export function useRegistry() { async ( data: Omit, ) => { - // Fetch fresh entries to prevent duplicate numbers from stale state + // Fetch fresh entries to prevent duplicate numbers from stale state. + // This single call replaces what was previously list() + N×get(). const freshEntries = await getAllEntries(storage); const now = new Date().toISOString(); const number = generateRegistryNumber( @@ -76,10 +77,11 @@ export function useRegistry() { updatedAt: now, }; await saveEntry(storage, entry); - await refresh(); + // Update local state directly to avoid a second full fetch + setEntries((prev) => [entry, ...prev]); return entry; }, - [storage, refresh], + [storage], ); const updateEntry = useCallback( @@ -108,29 +110,36 @@ export function useRegistry() { [storage, refresh], ); - /** Close an entry and optionally its linked entries */ + /** Close an entry and optionally its linked entries. + * Batches all saves, then does a single refresh at the end. + */ const closeEntry = useCallback( async (id: string, closeLinked: boolean) => { const entry = entries.find((e) => e.id === id); if (!entry) return; - await updateEntry(id, { status: "inchis" }); + // Save main entry as closed + const now = new Date().toISOString(); + const closedMain: RegistryEntry = { + ...entry, + status: "inchis", + updatedAt: now, + }; + await saveEntry(storage, closedMain); + // Close linked entries in parallel const linked = entry.linkedEntryIds ?? []; if (closeLinked && linked.length > 0) { - for (const linkedId of linked) { - const linked = entries.find((e) => e.id === linkedId); - if (linked && linked.status !== "inchis") { - const updatedLinked: RegistryEntry = { - ...linked, - status: "inchis", - updatedAt: new Date().toISOString(), - }; - await saveEntry(storage, updatedLinked); - } - } - await refresh(); + const saves = linked + .map((linkedId) => entries.find((e) => e.id === linkedId)) + .filter((e): e is RegistryEntry => !!e && e.status !== "inchis") + .map((e) => + saveEntry(storage, { ...e, status: "inchis", updatedAt: now }), + ); + await Promise.all(saves); } + // Single refresh at the end + await refresh(); }, - [entries, updateEntry, storage, refresh], + [entries, storage, refresh], ); const updateFilter = useCallback( diff --git a/src/modules/registratura/services/registry-service.ts b/src/modules/registratura/services/registry-service.ts index 54dc621..c642e44 100644 --- a/src/modules/registratura/services/registry-service.ts +++ b/src/modules/registratura/services/registry-service.ts @@ -8,17 +8,22 @@ export interface RegistryStorage { set(key: string, value: T): Promise; delete(key: string): Promise; list(): Promise; + exportAll(): Promise>; } +/** + * Load all registry entries in a SINGLE request. + * Uses exportAll() which fetches namespace data in one HTTP call, + * avoiding the N+1 pattern (list keys → get each one individually). + */ export async function getAllEntries( storage: RegistryStorage, ): Promise { - const keys = await storage.list(); + const all = await storage.exportAll(); const entries: RegistryEntry[] = []; - for (const key of keys) { - if (key.startsWith(STORAGE_PREFIX)) { - const entry = await storage.get(key); - if (entry) entries.push(entry); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(STORAGE_PREFIX) && value) { + entries.push(value as RegistryEntry); } } entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); diff --git a/src/modules/word-templates/hooks/use-templates.ts b/src/modules/word-templates/hooks/use-templates.ts index 9991430..e814822 100644 --- a/src/modules/word-templates/hooks/use-templates.ts +++ b/src/modules/word-templates/hooks/use-templates.ts @@ -1,32 +1,35 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback } from 'react'; -import { useStorage } from '@/core/storage'; -import { v4 as uuid } from 'uuid'; -import type { WordTemplate, TemplateCategory } from '../types'; +import { useState, useEffect, useCallback } from "react"; +import { useStorage } from "@/core/storage"; +import { v4 as uuid } from "uuid"; +import type { WordTemplate, TemplateCategory } from "../types"; -const PREFIX = 'tpl:'; +const PREFIX = "tpl:"; export interface TemplateFilters { search: string; - category: TemplateCategory | 'all'; + category: TemplateCategory | "all"; company: string; } export function useTemplates() { - const storage = useStorage('word-templates'); + const storage = useStorage("word-templates"); const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(true); - const [filters, setFilters] = useState({ search: '', category: 'all', company: 'all' }); + const [filters, setFilters] = useState({ + search: "", + category: "all", + company: "all", + }); const refresh = useCallback(async () => { setLoading(true); - const keys = await storage.list(); + const all = await storage.exportAll(); const results: WordTemplate[] = []; - for (const key of keys) { - if (key.startsWith(PREFIX)) { - const item = await storage.get(key); - if (item) results.push(item); + for (const [key, value] of Object.entries(all)) { + if (key.startsWith(PREFIX) && value) { + results.push(value as WordTemplate); } } results.sort((a, b) => a.name.localeCompare(b.name)); @@ -35,63 +38,103 @@ export function useTemplates() { }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { + refresh(); + }, [refresh]); - const addTemplate = useCallback(async (data: Omit) => { - const now = new Date().toISOString(); - const template: WordTemplate = { ...data, id: uuid(), createdAt: now, updatedAt: now }; - await storage.set(`${PREFIX}${template.id}`, template); - await refresh(); - return template; - }, [storage, refresh]); + const addTemplate = useCallback( + async (data: Omit) => { + const now = new Date().toISOString(); + const template: WordTemplate = { + ...data, + id: uuid(), + createdAt: now, + updatedAt: now, + }; + await storage.set(`${PREFIX}${template.id}`, template); + await refresh(); + return template; + }, + [storage, refresh], + ); - const updateTemplate = useCallback(async (id: string, updates: Partial) => { - const existing = templates.find((t) => t.id === id); - if (!existing) return; - const updated: WordTemplate = { - ...existing, ...updates, - id: existing.id, createdAt: existing.createdAt, - updatedAt: new Date().toISOString(), - }; - await storage.set(`${PREFIX}${id}`, updated); - await refresh(); - }, [storage, refresh, templates]); + const updateTemplate = useCallback( + async (id: string, updates: Partial) => { + const existing = templates.find((t) => t.id === id); + if (!existing) return; + const updated: WordTemplate = { + ...existing, + ...updates, + id: existing.id, + createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; + await storage.set(`${PREFIX}${id}`, updated); + await refresh(); + }, + [storage, refresh, templates], + ); - const cloneTemplate = useCallback(async (id: string) => { - const existing = templates.find((t) => t.id === id); - if (!existing) return; - const now = new Date().toISOString(); - const cloned: WordTemplate = { - ...existing, - id: uuid(), - name: `${existing.name} (copie)`, - clonedFrom: existing.id, - createdAt: now, - updatedAt: now, - }; - await storage.set(`${PREFIX}${cloned.id}`, cloned); - await refresh(); - return cloned; - }, [storage, refresh, templates]); + const cloneTemplate = useCallback( + async (id: string) => { + const existing = templates.find((t) => t.id === id); + if (!existing) return; + const now = new Date().toISOString(); + const cloned: WordTemplate = { + ...existing, + id: uuid(), + name: `${existing.name} (copie)`, + clonedFrom: existing.id, + createdAt: now, + updatedAt: now, + }; + await storage.set(`${PREFIX}${cloned.id}`, cloned); + await refresh(); + return cloned; + }, + [storage, refresh, templates], + ); - const removeTemplate = useCallback(async (id: string) => { - await storage.delete(`${PREFIX}${id}`); - await refresh(); - }, [storage, refresh]); + const removeTemplate = useCallback( + async (id: string) => { + await storage.delete(`${PREFIX}${id}`); + await refresh(); + }, + [storage, refresh], + ); - const updateFilter = useCallback((key: K, value: TemplateFilters[K]) => { - setFilters((prev) => ({ ...prev, [key]: value })); - }, []); + const updateFilter = useCallback( + (key: K, value: TemplateFilters[K]) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, + [], + ); const filteredTemplates = templates.filter((t) => { - if (filters.category !== 'all' && t.category !== filters.category) return false; - if (filters.company !== 'all' && t.company !== filters.company) return false; + if (filters.category !== "all" && t.category !== filters.category) + return false; + if (filters.company !== "all" && t.company !== filters.company) + return false; if (filters.search) { const q = filters.search.toLowerCase(); - return t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q); + return ( + t.name.toLowerCase().includes(q) || + t.description.toLowerCase().includes(q) + ); } return true; }); - return { templates: filteredTemplates, allTemplates: templates, loading, filters, updateFilter, addTemplate, updateTemplate, cloneTemplate, removeTemplate, refresh }; + return { + templates: filteredTemplates, + allTemplates: templates, + loading, + filters, + updateFilter, + addTemplate, + updateTemplate, + cloneTemplate, + removeTemplate, + refresh, + }; }