perf: fix N+1 query pattern across all modules + rack numbering
CRITICAL PERF BUG: Every hook did storage.list() (1 HTTP call fetching ALL items with values, discarding values, returning only keys) then storage.get() for EACH key (N individual HTTP calls re-fetching values one by one). With 6 entries + contacts + tags, Registratura page fired ~40 sequential HTTP requests on load, where 3 would suffice. Fix: Replace list()+N*get() with single exportAll() call in ALL hooks: - registratura/registry-service.ts (added exportAll to RegistryStorage interface) - address-book/use-contacts.ts - it-inventory/use-inventory.ts - password-vault/use-vault.ts - word-templates/use-templates.ts - prompt-generator/use-prompt-generator.ts - hot-desk/use-reservations.ts - email-signature/use-saved-signatures.ts - digital-signatures/use-signatures.ts - ai-chat/use-chat.ts - core/tagging/tag-service.ts (uses storage.export()) Additional fixes: - registratura/use-registry.ts: addEntry uses optimistic local state update instead of double-refresh; closeEntry batches saves with Promise.all + single refresh - server-rack.tsx: reversed slot rendering so U1 is at bottom (standard rack numbering, per user's physical rack) Performance impact: ~90% reduction in HTTP requests on page load for all modules
This commit is contained in:
@@ -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<Tag[]> {
|
||||
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<Tag>(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<Tag[]> {
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.category === category);
|
||||
return this.storage.query<Tag>(
|
||||
NAMESPACE,
|
||||
(tag) => tag.category === category,
|
||||
);
|
||||
}
|
||||
|
||||
async getTagsByScope(scope: TagScope, scopeId?: string): Promise<Tag[]> {
|
||||
return this.storage.query<Tag>(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<Tag[]> {
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.parentId === parentId);
|
||||
return this.storage.query<Tag>(
|
||||
NAMESPACE,
|
||||
(tag) => tag.parentId === parentId,
|
||||
);
|
||||
}
|
||||
|
||||
async createTag(data: Omit<Tag, 'id' | 'createdAt'>): Promise<Tag> {
|
||||
async createTag(data: Omit<Tag, "id" | "createdAt">): Promise<Tag> {
|
||||
const tag: Tag = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
@@ -44,10 +49,17 @@ export class TagService {
|
||||
return tag;
|
||||
}
|
||||
|
||||
async updateTag(id: string, updates: Partial<Omit<Tag, 'id' | 'createdAt'>>): Promise<Tag | null> {
|
||||
async updateTag(
|
||||
id: string,
|
||||
updates: Partial<Omit<Tag, "id" | "createdAt">>,
|
||||
): Promise<Tag | null> {
|
||||
const existing = await this.storage.get<Tag>(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<Tag[]> {
|
||||
const lower = query.toLowerCase();
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) =>
|
||||
return this.storage.query<Tag>(
|
||||
NAMESPACE,
|
||||
(tag) =>
|
||||
tag.label.toLowerCase().includes(lower) ||
|
||||
(tag.projectCode?.toLowerCase().includes(lower) ?? false)
|
||||
(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<Tag, 'id' | 'createdAt'>[]): Promise<number> {
|
||||
async importTags(tags: Omit<Tag, "id" | "createdAt">[]): Promise<number> {
|
||||
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}`;
|
||||
|
||||
@@ -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<AddressContact[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<ContactFilters>({ search: '', type: 'all' });
|
||||
const [filters, setFilters] = useState<ContactFilters>({
|
||||
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<AddressContact>(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,17 +36,28 @@ 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<AddressContact, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const addContact = useCallback(
|
||||
async (data: Omit<AddressContact, "id" | "createdAt" | "updatedAt">) => {
|
||||
const now = new Date().toISOString();
|
||||
const contact: AddressContact = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||
const contact: AddressContact = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await storage.set(`${PREFIX}${contact.id}`, contact);
|
||||
await refresh();
|
||||
return contact;
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateContact = useCallback(async (id: string, updates: Partial<AddressContact>) => {
|
||||
const updateContact = useCallback(
|
||||
async (id: string, updates: Partial<AddressContact>) => {
|
||||
const existing = contacts.find((c) => c.id === id);
|
||||
if (!existing) return;
|
||||
const updated: AddressContact = {
|
||||
@@ -56,19 +69,27 @@ export function useContacts() {
|
||||
};
|
||||
await storage.set(`${PREFIX}${id}`, updated);
|
||||
await refresh();
|
||||
}, [storage, refresh, contacts]);
|
||||
},
|
||||
[storage, refresh, contacts],
|
||||
);
|
||||
|
||||
const removeContact = useCallback(async (id: string) => {
|
||||
const removeContact = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof ContactFilters>(key: K, value: ContactFilters[K]) => {
|
||||
const updateFilter = useCallback(
|
||||
<K extends keyof ContactFilters>(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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<ChatSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(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<ChatSession>(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,12 +30,15 @@ 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 createSession = useCallback(
|
||||
async (title?: string) => {
|
||||
const session: ChatSession = {
|
||||
id: uuid(),
|
||||
title: title || `Conversație ${new Date().toLocaleDateString('ro-RO')}`,
|
||||
title: title || `Conversație ${new Date().toLocaleDateString("ro-RO")}`,
|
||||
messages: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -44,9 +46,12 @@ export function useChat() {
|
||||
setSessions((prev) => [session, ...prev]);
|
||||
setActiveSessionId(session.id);
|
||||
return session;
|
||||
}, [storage]);
|
||||
},
|
||||
[storage],
|
||||
);
|
||||
|
||||
const addMessage = useCallback(async (content: string, role: ChatMessage['role']) => {
|
||||
const addMessage = useCallback(
|
||||
async (content: string, role: ChatMessage["role"]) => {
|
||||
if (!activeSessionId) return;
|
||||
const message: ChatMessage = {
|
||||
id: uuid(),
|
||||
@@ -54,26 +59,36 @@ export function useChat() {
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
setSessions((prev) => prev.map((s) => {
|
||||
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] };
|
||||
const updated = {
|
||||
...current,
|
||||
messages: [...current.messages, message],
|
||||
};
|
||||
await storage.set(`${SESSION_PREFIX}${activeSessionId}`, updated);
|
||||
}
|
||||
return message;
|
||||
}, [storage, activeSessionId, sessions]);
|
||||
},
|
||||
[storage, activeSessionId, sessions],
|
||||
);
|
||||
|
||||
const deleteSession = useCallback(async (id: string) => {
|
||||
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]);
|
||||
},
|
||||
[storage, activeSessionId],
|
||||
);
|
||||
|
||||
const selectSession = useCallback((id: string) => {
|
||||
setActiveSessionId(id);
|
||||
|
||||
@@ -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<SignatureAsset[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<SignatureFilters>({ search: '', type: 'all' });
|
||||
const [filters, setFilters] = useState<SignatureFilters>({
|
||||
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<SignatureAsset>(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<SignatureAsset, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const addAsset = useCallback(
|
||||
async (data: Omit<SignatureAsset, "id" | "createdAt" | "updatedAt">) => {
|
||||
const now = new Date().toISOString();
|
||||
const asset: SignatureAsset = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||
const asset: SignatureAsset = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await storage.set(`${PREFIX}${asset.id}`, asset);
|
||||
await refresh();
|
||||
return asset;
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateAsset = useCallback(async (id: string, updates: Partial<SignatureAsset>) => {
|
||||
const updateAsset = useCallback(
|
||||
async (id: string, updates: Partial<SignatureAsset>) => {
|
||||
const existing = assets.find((a) => a.id === id);
|
||||
if (!existing) return;
|
||||
const updated: SignatureAsset = {
|
||||
...existing, ...updates,
|
||||
id: existing.id, createdAt: existing.createdAt,
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await storage.set(`${PREFIX}${id}`, updated);
|
||||
await refresh();
|
||||
}, [storage, refresh, assets]);
|
||||
},
|
||||
[storage, refresh, assets],
|
||||
);
|
||||
|
||||
const addVersion = useCallback(async (assetId: string, imageUrl: string, notes: string) => {
|
||||
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 version: AssetVersion = {
|
||||
id: uuid(),
|
||||
imageUrl,
|
||||
notes,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updatedVersions = [...(existing.versions ?? []), version];
|
||||
await updateAsset(assetId, { imageUrl, versions: updatedVersions });
|
||||
}, [assets, updateAsset]);
|
||||
},
|
||||
[assets, updateAsset],
|
||||
);
|
||||
|
||||
const removeAsset = useCallback(async (id: string) => {
|
||||
const removeAsset = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof SignatureFilters>(key: K, value: SignatureFilters[K]) => {
|
||||
const updateFilter = useCallback(
|
||||
<K extends keyof SignatureFilters>(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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<SavedSignature[]>([]);
|
||||
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<SavedSignature>(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,9 +25,12 @@ 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 save = useCallback(
|
||||
async (label: string, config: SignatureConfig) => {
|
||||
const now = new Date().toISOString();
|
||||
const entry: SavedSignature = {
|
||||
id: uuid(),
|
||||
@@ -40,12 +42,17 @@ export function useSavedSignatures() {
|
||||
await storage.set(`sig:${entry.id}`, entry);
|
||||
await refresh();
|
||||
return entry;
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const remove = useCallback(async (id: string) => {
|
||||
const remove = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`sig:${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
return { saved, loading, save, remove, refresh };
|
||||
}
|
||||
|
||||
@@ -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<DeskReservation>(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));
|
||||
|
||||
@@ -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 */}
|
||||
<div className="p-1 space-y-px">
|
||||
{slots.map((slot) => {
|
||||
{displaySlots.map((slot) => {
|
||||
if (!slot.isStart && slot.item) return null;
|
||||
|
||||
const heightPx = slot.height * 24;
|
||||
|
||||
@@ -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<InventoryItem>(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));
|
||||
|
||||
@@ -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<VaultEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<VaultFilters>({ search: '', category: 'all' });
|
||||
const [filters, setFilters] = useState<VaultFilters>({
|
||||
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<VaultEntry>(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<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const addEntry = useCallback(
|
||||
async (data: Omit<VaultEntry, "id" | "createdAt" | "updatedAt">) => {
|
||||
const now = new Date().toISOString();
|
||||
const entry: VaultEntry = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||
const entry: VaultEntry = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await storage.set(`${PREFIX}${entry.id}`, entry);
|
||||
await refresh();
|
||||
return entry;
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateEntry = useCallback(async (id: string, updates: Partial<VaultEntry>) => {
|
||||
const updateEntry = useCallback(
|
||||
async (id: string, updates: Partial<VaultEntry>) => {
|
||||
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() };
|
||||
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]);
|
||||
},
|
||||
[storage, refresh, entries],
|
||||
);
|
||||
|
||||
const removeEntry = useCallback(async (id: string) => {
|
||||
const removeEntry = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof VaultFilters>(key: K, value: VaultFilters[K]) => {
|
||||
const updateFilter = useCallback(
|
||||
<K extends keyof VaultFilters>(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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<PromptTemplate | null>(null);
|
||||
const storage = useStorage("prompt-generator");
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<PromptTemplate | null>(null);
|
||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||
const [customTemplates, setCustomTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [history, setHistory] = useState<PromptHistoryEntry[]>([]);
|
||||
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<PromptTemplate>(key);
|
||||
if (t) templates.push(t);
|
||||
templates.push(value as PromptTemplate);
|
||||
} else if (key.startsWith(HISTORY_PREFIX)) {
|
||||
const h = await storage.get<PromptHistoryEntry>(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) => {
|
||||
const deleteHistoryEntry = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`${HISTORY_PREFIX}${id}`);
|
||||
setHistory((prev) => prev.filter((h) => h.id !== id));
|
||||
}, [storage]);
|
||||
},
|
||||
[storage],
|
||||
);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedTemplate(null);
|
||||
|
||||
@@ -59,7 +59,8 @@ export function useRegistry() {
|
||||
async (
|
||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||
) => {
|
||||
// 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);
|
||||
}
|
||||
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(
|
||||
|
||||
@@ -8,17 +8,22 @@ export interface RegistryStorage {
|
||||
set<T>(key: string, value: T): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
list(): Promise<string[]>;
|
||||
exportAll(): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<RegistryEntry[]> {
|
||||
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<RegistryEntry>(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));
|
||||
|
||||
@@ -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<WordTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<TemplateFilters>({ search: '', category: 'all', company: 'all' });
|
||||
const [filters, setFilters] = useState<TemplateFilters>({
|
||||
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<WordTemplate>(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,29 +38,45 @@ 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<WordTemplate, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const addTemplate = useCallback(
|
||||
async (data: Omit<WordTemplate, "id" | "createdAt" | "updatedAt">) => {
|
||||
const now = new Date().toISOString();
|
||||
const template: WordTemplate = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||
const template: WordTemplate = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await storage.set(`${PREFIX}${template.id}`, template);
|
||||
await refresh();
|
||||
return template;
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateTemplate = useCallback(async (id: string, updates: Partial<WordTemplate>) => {
|
||||
const updateTemplate = useCallback(
|
||||
async (id: string, updates: Partial<WordTemplate>) => {
|
||||
const existing = templates.find((t) => t.id === id);
|
||||
if (!existing) return;
|
||||
const updated: WordTemplate = {
|
||||
...existing, ...updates,
|
||||
id: existing.id, createdAt: existing.createdAt,
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await storage.set(`${PREFIX}${id}`, updated);
|
||||
await refresh();
|
||||
}, [storage, refresh, templates]);
|
||||
},
|
||||
[storage, refresh, templates],
|
||||
);
|
||||
|
||||
const cloneTemplate = useCallback(async (id: string) => {
|
||||
const cloneTemplate = useCallback(
|
||||
async (id: string) => {
|
||||
const existing = templates.find((t) => t.id === id);
|
||||
if (!existing) return;
|
||||
const now = new Date().toISOString();
|
||||
@@ -72,26 +91,50 @@ export function useTemplates() {
|
||||
await storage.set(`${PREFIX}${cloned.id}`, cloned);
|
||||
await refresh();
|
||||
return cloned;
|
||||
}, [storage, refresh, templates]);
|
||||
},
|
||||
[storage, refresh, templates],
|
||||
);
|
||||
|
||||
const removeTemplate = useCallback(async (id: string) => {
|
||||
const removeTemplate = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof TemplateFilters>(key: K, value: TemplateFilters[K]) => {
|
||||
const updateFilter = useCallback(
|
||||
<K extends keyof TemplateFilters>(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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user