|
|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
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 { Input } from '@/shared/components/ui/input';
|
|
|
|
|
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 { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
|
|
|
|
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 { WordTemplate } from '../types';
|
|
|
|
|
import type { WordTemplate, TemplateCategory } from '../types';
|
|
|
|
|
import { useTemplates } from '../hooks/use-templates';
|
|
|
|
|
|
|
|
|
|
const TEMPLATE_CATEGORIES = [
|
|
|
|
|
'Contract', 'Memoriu tehnic', 'Ofertă', 'Factură', 'Raport', 'Deviz', 'Proces-verbal', 'Altele',
|
|
|
|
|
];
|
|
|
|
|
const CATEGORY_LABELS: Record<TemplateCategory, string> = {
|
|
|
|
|
contract: 'Contract',
|
|
|
|
|
memoriu: 'Memoriu tehnic',
|
|
|
|
|
oferta: 'Ofertă',
|
|
|
|
|
raport: 'Raport',
|
|
|
|
|
cerere: 'Cerere',
|
|
|
|
|
aviz: 'Aviz',
|
|
|
|
|
scrisoare: 'Scrisoare',
|
|
|
|
|
altele: 'Altele',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ViewMode = 'list' | 'add' | 'edit';
|
|
|
|
|
|
|
|
|
|
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 [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) {
|
|
|
|
|
await updateTemplate(editingTemplate.id, data);
|
|
|
|
|
} else {
|
|
|
|
|
@@ -34,16 +43,21 @@ export function WordTemplatesModule() {
|
|
|
|
|
setEditingTemplate(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const filterCategories = allCategories.length > 0 ? allCategories : TEMPLATE_CATEGORIES;
|
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
|
|
|
if (deletingId) {
|
|
|
|
|
await removeTemplate(deletingId);
|
|
|
|
|
setDeletingId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Stats */}
|
|
|
|
|
<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">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">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>
|
|
|
|
|
|
|
|
|
|
{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" />
|
|
|
|
|
<Input placeholder="Caută șablon..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
|
|
|
|
|
</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>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="all">Toate</SelectItem>
|
|
|
|
|
{filterCategories.map((c) => (
|
|
|
|
|
<SelectItem key={c} value={c}>{c}</SelectItem>
|
|
|
|
|
<SelectItem value="all">Toate categoriile</SelectItem>
|
|
|
|
|
{(Object.keys(CATEGORY_LABELS) as TemplateCategory[]).map((c) => (
|
|
|
|
|
<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</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">
|
|
|
|
|
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
|
|
|
|
</Button>
|
|
|
|
|
@@ -70,19 +94,20 @@ export function WordTemplatesModule() {
|
|
|
|
|
{loading ? (
|
|
|
|
|
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
|
|
|
|
) : templates.length === 0 ? (
|
|
|
|
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
|
|
|
Niciun șablon găsit. Adaugă primul șablon Word.
|
|
|
|
|
</p>
|
|
|
|
|
<p className="py-8 text-center text-sm text-muted-foreground">Niciun șablon găsit. Adaugă primul șablon Word.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{templates.map((tpl) => (
|
|
|
|
|
<Card key={tpl.id} className="group relative">
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
<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'); }}>
|
|
|
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
|
|
|
</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" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -94,9 +119,18 @@ export function WordTemplatesModule() {
|
|
|
|
|
<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>}
|
|
|
|
|
<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>
|
|
|
|
|
{tpl.clonedFrom && <Badge variant="secondary" className="text-[10px]">Clonă</Badge>}
|
|
|
|
|
</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 && (
|
|
|
|
|
<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
|
|
|
|
|
@@ -120,30 +154,58 @@ export function WordTemplatesModule() {
|
|
|
|
|
</CardContent>
|
|
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TemplateForm({ initial, onSubmit, onCancel }: {
|
|
|
|
|
initial?: WordTemplate;
|
|
|
|
|
onSubmit: (data: Omit<WordTemplate, 'id' | 'createdAt'>) => void;
|
|
|
|
|
onSubmit: (data: Omit<WordTemplate, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
const [name, setName] = useState(initial?.name ?? '');
|
|
|
|
|
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 [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
|
|
|
|
const [version, setVersion] = useState(initial?.version ?? '1.0.0');
|
|
|
|
|
const [placeholdersText, setPlaceholdersText] = useState(initial?.placeholders.join(', ') ?? '');
|
|
|
|
|
|
|
|
|
|
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><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>
|
|
|
|
|
<Select value={category} onValueChange={setCategory}>
|
|
|
|
|
<Select value={category} onValueChange={(v) => setCategory(v as TemplateCategory)}>
|
|
|
|
|
<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>
|
|
|
|
|
</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>URL fișier</Label><Input value={fileUrl} onChange={(e) => setFileUrl(e.target.value)} className="mt-1" placeholder="https://..." /></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">
|
|
|
|
|
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
|
|
|
|
|
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
|
|
|
|
|
|