feat(word-templates): add company pools, template cloning, typed categories, and placeholder display
- TemplateCategory union type (8 values) replacing plain string
- Company-specific filtering in template list
- Template cloning with clone badge indicator
- Placeholder display ({{VARIABLE}} markers) in card and form
- Delete confirmation dialog
- updatedAt timestamp support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Plus, Pencil, Trash2, Search, FileText, ExternalLink } from 'lucide-react';
|
import { Plus, Pencil, Trash2, Search, FileText, ExternalLink, Copy } from 'lucide-react';
|
||||||
import { Button } from '@/shared/components/ui/button';
|
import { Button } from '@/shared/components/ui/button';
|
||||||
import { Input } from '@/shared/components/ui/input';
|
import { Input } from '@/shared/components/ui/input';
|
||||||
import { Label } from '@/shared/components/ui/label';
|
import { Label } from '@/shared/components/ui/label';
|
||||||
@@ -9,22 +9,31 @@ import { Textarea } from '@/shared/components/ui/textarea';
|
|||||||
import { Badge } from '@/shared/components/ui/badge';
|
import { Badge } from '@/shared/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/shared/components/ui/dialog';
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
import type { CompanyId } from '@/core/auth/types';
|
||||||
import type { WordTemplate } from '../types';
|
import type { WordTemplate, TemplateCategory } from '../types';
|
||||||
import { useTemplates } from '../hooks/use-templates';
|
import { useTemplates } from '../hooks/use-templates';
|
||||||
|
|
||||||
const TEMPLATE_CATEGORIES = [
|
const CATEGORY_LABELS: Record<TemplateCategory, string> = {
|
||||||
'Contract', 'Memoriu tehnic', 'Ofertă', 'Factură', 'Raport', 'Deviz', 'Proces-verbal', 'Altele',
|
contract: 'Contract',
|
||||||
];
|
memoriu: 'Memoriu tehnic',
|
||||||
|
oferta: 'Ofertă',
|
||||||
|
raport: 'Raport',
|
||||||
|
cerere: 'Cerere',
|
||||||
|
aviz: 'Aviz',
|
||||||
|
scrisoare: 'Scrisoare',
|
||||||
|
altele: 'Altele',
|
||||||
|
};
|
||||||
|
|
||||||
type ViewMode = 'list' | 'add' | 'edit';
|
type ViewMode = 'list' | 'add' | 'edit';
|
||||||
|
|
||||||
export function WordTemplatesModule() {
|
export function WordTemplatesModule() {
|
||||||
const { templates, allTemplates, allCategories, loading, filters, updateFilter, addTemplate, updateTemplate, removeTemplate } = useTemplates();
|
const { templates, allTemplates, loading, filters, updateFilter, addTemplate, updateTemplate, cloneTemplate, removeTemplate } = useTemplates();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||||
const [editingTemplate, setEditingTemplate] = useState<WordTemplate | null>(null);
|
const [editingTemplate, setEditingTemplate] = useState<WordTemplate | null>(null);
|
||||||
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSubmit = async (data: Omit<WordTemplate, 'id' | 'createdAt'>) => {
|
const handleSubmit = async (data: Omit<WordTemplate, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
if (viewMode === 'edit' && editingTemplate) {
|
if (viewMode === 'edit' && editingTemplate) {
|
||||||
await updateTemplate(editingTemplate.id, data);
|
await updateTemplate(editingTemplate.id, data);
|
||||||
} else {
|
} else {
|
||||||
@@ -34,16 +43,21 @@ export function WordTemplatesModule() {
|
|||||||
setEditingTemplate(null);
|
setEditingTemplate(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterCategories = allCategories.length > 0 ? allCategories : TEMPLATE_CATEGORIES;
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (deletingId) {
|
||||||
|
await removeTemplate(deletingId);
|
||||||
|
setDeletingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total șabloane</p><p className="text-2xl font-bold">{allTemplates.length}</p></CardContent></Card>
|
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total șabloane</p><p className="text-2xl font-bold">{allTemplates.length}</p></CardContent></Card>
|
||||||
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Categorii</p><p className="text-2xl font-bold">{allCategories.length}</p></CardContent></Card>
|
|
||||||
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Beletage</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'beletage').length}</p></CardContent></Card>
|
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Beletage</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'beletage').length}</p></CardContent></Card>
|
||||||
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Urban Switch</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'urban-switch').length}</p></CardContent></Card>
|
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Urban Switch</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'urban-switch').length}</p></CardContent></Card>
|
||||||
|
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Studii de Teren</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'studii-de-teren').length}</p></CardContent></Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'list' && (
|
{viewMode === 'list' && (
|
||||||
@@ -53,15 +67,25 @@ export function WordTemplatesModule() {
|
|||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input placeholder="Caută șablon..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
|
<Input placeholder="Caută șablon..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
|
||||||
</div>
|
</div>
|
||||||
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v)}>
|
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v as TemplateCategory | 'all')}>
|
||||||
<SelectTrigger className="w-[160px]"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-[160px]"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Toate</SelectItem>
|
<SelectItem value="all">Toate categoriile</SelectItem>
|
||||||
{filterCategories.map((c) => (
|
{(Object.keys(CATEGORY_LABELS) as TemplateCategory[]).map((c) => (
|
||||||
<SelectItem key={c} value={c}>{c}</SelectItem>
|
<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select value={filters.company} onValueChange={(v) => updateFilter('company', v)}>
|
||||||
|
<SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Toate companiile</SelectItem>
|
||||||
|
<SelectItem value="beletage">Beletage</SelectItem>
|
||||||
|
<SelectItem value="urban-switch">Urban Switch</SelectItem>
|
||||||
|
<SelectItem value="studii-de-teren">Studii de Teren</SelectItem>
|
||||||
|
<SelectItem value="group">Grup</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
||||||
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
||||||
</Button>
|
</Button>
|
||||||
@@ -70,19 +94,20 @@ export function WordTemplatesModule() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
||||||
) : templates.length === 0 ? (
|
) : templates.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
<p className="py-8 text-center text-sm text-muted-foreground">Niciun șablon găsit. Adaugă primul șablon Word.</p>
|
||||||
Niciun șablon găsit. Adaugă primul șablon Word.
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{templates.map((tpl) => (
|
{templates.map((tpl) => (
|
||||||
<Card key={tpl.id} className="group relative">
|
<Card key={tpl.id} className="group relative">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7" title="Clonează" onClick={() => cloneTemplate(tpl.id)}>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingTemplate(tpl); setViewMode('edit'); }}>
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingTemplate(tpl); setViewMode('edit'); }}>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeTemplate(tpl.id)}>
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => setDeletingId(tpl.id)}>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,9 +119,18 @@ export function WordTemplatesModule() {
|
|||||||
<p className="font-medium">{tpl.name}</p>
|
<p className="font-medium">{tpl.name}</p>
|
||||||
{tpl.description && <p className="mt-0.5 text-xs text-muted-foreground line-clamp-2">{tpl.description}</p>}
|
{tpl.description && <p className="mt-0.5 text-xs text-muted-foreground line-clamp-2">{tpl.description}</p>}
|
||||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||||
{tpl.category && <Badge variant="outline" className="text-[10px]">{tpl.category}</Badge>}
|
<Badge variant="outline" className="text-[10px]">{CATEGORY_LABELS[tpl.category]}</Badge>
|
||||||
<Badge variant="secondary" className="text-[10px]">v{tpl.version}</Badge>
|
<Badge variant="secondary" className="text-[10px]">v{tpl.version}</Badge>
|
||||||
|
{tpl.clonedFrom && <Badge variant="secondary" className="text-[10px]">Clonă</Badge>}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Placeholders display */}
|
||||||
|
{tpl.placeholders.length > 0 && (
|
||||||
|
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||||
|
{tpl.placeholders.map((p) => (
|
||||||
|
<span key={p} className="rounded bg-muted px-1 py-0.5 font-mono text-[10px] text-muted-foreground">{`{{${p}}}`}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{tpl.fileUrl && (
|
{tpl.fileUrl && (
|
||||||
<a href={tpl.fileUrl} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 text-xs text-primary hover:underline">
|
<a href={tpl.fileUrl} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 text-xs text-primary hover:underline">
|
||||||
<ExternalLink className="h-3 w-3" /> Deschide fișier
|
<ExternalLink className="h-3 w-3" /> Deschide fișier
|
||||||
@@ -120,30 +154,58 @@ export function WordTemplatesModule() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete confirmation */}
|
||||||
|
<Dialog open={deletingId !== null} onOpenChange={(open) => { if (!open) setDeletingId(null); }}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader><DialogTitle>Confirmare ștergere</DialogTitle></DialogHeader>
|
||||||
|
<p className="text-sm">Ești sigur că vrei să ștergi acest șablon? Acțiunea este ireversibilă.</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeletingId(null)}>Anulează</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteConfirm}>Șterge</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TemplateForm({ initial, onSubmit, onCancel }: {
|
function TemplateForm({ initial, onSubmit, onCancel }: {
|
||||||
initial?: WordTemplate;
|
initial?: WordTemplate;
|
||||||
onSubmit: (data: Omit<WordTemplate, 'id' | 'createdAt'>) => void;
|
onSubmit: (data: Omit<WordTemplate, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [name, setName] = useState(initial?.name ?? '');
|
const [name, setName] = useState(initial?.name ?? '');
|
||||||
const [description, setDescription] = useState(initial?.description ?? '');
|
const [description, setDescription] = useState(initial?.description ?? '');
|
||||||
const [category, setCategory] = useState(initial?.category ?? 'Contract');
|
const [category, setCategory] = useState<TemplateCategory>(initial?.category ?? 'contract');
|
||||||
const [fileUrl, setFileUrl] = useState(initial?.fileUrl ?? '');
|
const [fileUrl, setFileUrl] = useState(initial?.fileUrl ?? '');
|
||||||
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
||||||
const [version, setVersion] = useState(initial?.version ?? '1.0.0');
|
const [version, setVersion] = useState(initial?.version ?? '1.0.0');
|
||||||
|
const [placeholdersText, setPlaceholdersText] = useState(initial?.placeholders.join(', ') ?? '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ name, description, category, fileUrl, company, version, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
|
<form onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const placeholders = placeholdersText
|
||||||
|
.split(',')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter((p) => p.length > 0);
|
||||||
|
onSubmit({
|
||||||
|
name, description, category, fileUrl, company, version, placeholders,
|
||||||
|
clonedFrom: initial?.clonedFrom,
|
||||||
|
tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all',
|
||||||
|
});
|
||||||
|
}} className="space-y-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div><Label>Nume șablon</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
|
<div><Label>Nume șablon *</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
|
||||||
<div><Label>Categorie</Label>
|
<div><Label>Categorie</Label>
|
||||||
<Select value={category} onValueChange={setCategory}>
|
<Select value={category} onValueChange={(v) => setCategory(v as TemplateCategory)}>
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>{TEMPLATE_CATEGORIES.map((c) => (<SelectItem key={c} value={c}>{c}</SelectItem>))}</SelectContent>
|
<SelectContent>
|
||||||
|
{(Object.keys(CATEGORY_LABELS) as TemplateCategory[]).map((c) => (
|
||||||
|
<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,6 +225,11 @@ function TemplateForm({ initial, onSubmit, onCancel }: {
|
|||||||
<div><Label>Versiune</Label><Input value={version} onChange={(e) => setVersion(e.target.value)} className="mt-1" /></div>
|
<div><Label>Versiune</Label><Input value={version} onChange={(e) => setVersion(e.target.value)} className="mt-1" /></div>
|
||||||
<div><Label>URL fișier</Label><Input value={fileUrl} onChange={(e) => setFileUrl(e.target.value)} className="mt-1" placeholder="https://..." /></div>
|
<div><Label>URL fișier</Label><Input value={fileUrl} onChange={(e) => setFileUrl(e.target.value)} className="mt-1" placeholder="https://..." /></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Placeholder-e (separate prin virgulă)</Label>
|
||||||
|
<Input value={placeholdersText} onChange={(e) => setPlaceholdersText(e.target.value)} className="mt-1" placeholder="NUME_BENEFICIAR, DATA_CONTRACT, NR_PROIECT..." />
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">Variabilele din șablon, de forma {'{{VARIABILA}}'}, separate prin virgulă.</p>
|
||||||
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
||||||
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
||||||
|
|||||||
@@ -3,20 +3,21 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from '@/core/storage';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { WordTemplate } from '../types';
|
import type { WordTemplate, TemplateCategory } from '../types';
|
||||||
|
|
||||||
const PREFIX = 'tpl:';
|
const PREFIX = 'tpl:';
|
||||||
|
|
||||||
export interface TemplateFilters {
|
export interface TemplateFilters {
|
||||||
search: string;
|
search: string;
|
||||||
category: string;
|
category: TemplateCategory | 'all';
|
||||||
|
company: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTemplates() {
|
export function useTemplates() {
|
||||||
const storage = useStorage('word-templates');
|
const storage = useStorage('word-templates');
|
||||||
const [templates, setTemplates] = useState<WordTemplate[]>([]);
|
const [templates, setTemplates] = useState<WordTemplate[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState<TemplateFilters>({ search: '', category: 'all' });
|
const [filters, setFilters] = useState<TemplateFilters>({ search: '', category: 'all', company: 'all' });
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -36,8 +37,9 @@ export function useTemplates() {
|
|||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// 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'>) => {
|
const addTemplate = useCallback(async (data: Omit<WordTemplate, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
const template: WordTemplate = { ...data, id: uuid(), createdAt: new Date().toISOString() };
|
const now = new Date().toISOString();
|
||||||
|
const template: WordTemplate = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||||
await storage.set(`${PREFIX}${template.id}`, template);
|
await storage.set(`${PREFIX}${template.id}`, template);
|
||||||
await refresh();
|
await refresh();
|
||||||
return template;
|
return template;
|
||||||
@@ -46,11 +48,32 @@ export function useTemplates() {
|
|||||||
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);
|
const existing = templates.find((t) => t.id === id);
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt };
|
const updated: WordTemplate = {
|
||||||
|
...existing, ...updates,
|
||||||
|
id: existing.id, createdAt: existing.createdAt,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
await storage.set(`${PREFIX}${id}`, updated);
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
await refresh();
|
await refresh();
|
||||||
}, [storage, refresh, templates]);
|
}, [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) => {
|
const removeTemplate = useCallback(async (id: string) => {
|
||||||
await storage.delete(`${PREFIX}${id}`);
|
await storage.delete(`${PREFIX}${id}`);
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -60,10 +83,9 @@ export function useTemplates() {
|
|||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const allCategories = [...new Set(templates.map((t) => t.category).filter(Boolean))];
|
|
||||||
|
|
||||||
const filteredTemplates = templates.filter((t) => {
|
const filteredTemplates = templates.filter((t) => {
|
||||||
if (filters.category !== 'all' && t.category !== filters.category) 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) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
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);
|
||||||
@@ -71,5 +93,5 @@ export function useTemplates() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { templates: filteredTemplates, allTemplates: templates, allCategories, loading, filters, updateFilter, addTemplate, updateTemplate, removeTemplate, refresh };
|
return { templates: filteredTemplates, allTemplates: templates, loading, filters, updateFilter, addTemplate, updateTemplate, cloneTemplate, removeTemplate, refresh };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { wordTemplatesConfig } from './config';
|
export { wordTemplatesConfig } from './config';
|
||||||
export { WordTemplatesModule } from './components/word-templates-module';
|
export { WordTemplatesModule } from './components/word-templates-module';
|
||||||
export type { WordTemplate } from './types';
|
export type { WordTemplate, TemplateCategory } from './types';
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import type { Visibility } from '@/core/module-registry/types';
|
import type { Visibility } from '@/core/module-registry/types';
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
import type { CompanyId } from '@/core/auth/types';
|
||||||
|
|
||||||
|
export type TemplateCategory =
|
||||||
|
| 'contract'
|
||||||
|
| 'memoriu'
|
||||||
|
| 'oferta'
|
||||||
|
| 'raport'
|
||||||
|
| 'cerere'
|
||||||
|
| 'aviz'
|
||||||
|
| 'scrisoare'
|
||||||
|
| 'altele';
|
||||||
|
|
||||||
export interface WordTemplate {
|
export interface WordTemplate {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
category: string;
|
category: TemplateCategory;
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
company: CompanyId;
|
company: CompanyId;
|
||||||
|
/** Detected placeholders in template */
|
||||||
|
placeholders: string[];
|
||||||
|
/** Cloned from template ID */
|
||||||
|
clonedFrom?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
version: string;
|
version: string;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user