|
|
|
|
@@ -1,13 +1,15 @@
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { Plus, Pencil, Trash2, Search, PenTool, Stamp, Type } from 'lucide-react';
|
|
|
|
|
import { Plus, Pencil, Trash2, Search, PenTool, Stamp, Type, History, AlertTriangle } from 'lucide-react';
|
|
|
|
|
import { Button } from '@/shared/components/ui/button';
|
|
|
|
|
import { Input } from '@/shared/components/ui/input';
|
|
|
|
|
import { Label } from '@/shared/components/ui/label';
|
|
|
|
|
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 { SignatureAsset, SignatureAssetType } from '../types';
|
|
|
|
|
import { useSignatures } from '../hooks/use-signatures';
|
|
|
|
|
@@ -23,11 +25,13 @@ const TYPE_ICONS: Record<SignatureAssetType, typeof PenTool> = {
|
|
|
|
|
type ViewMode = 'list' | 'add' | 'edit';
|
|
|
|
|
|
|
|
|
|
export function DigitalSignaturesModule() {
|
|
|
|
|
const { assets, allAssets, loading, filters, updateFilter, addAsset, updateAsset, removeAsset } = useSignatures();
|
|
|
|
|
const { assets, allAssets, loading, filters, updateFilter, addAsset, updateAsset, addVersion, removeAsset } = useSignatures();
|
|
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
|
|
|
|
const [editingAsset, setEditingAsset] = useState<SignatureAsset | null>(null);
|
|
|
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
|
|
|
const [versionAsset, setVersionAsset] = useState<SignatureAsset | null>(null);
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (data: Omit<SignatureAsset, 'id' | 'createdAt'>) => {
|
|
|
|
|
const handleSubmit = async (data: Omit<SignatureAsset, 'id' | 'createdAt' | 'updatedAt'>) => {
|
|
|
|
|
if (viewMode === 'edit' && editingAsset) {
|
|
|
|
|
await updateAsset(editingAsset.id, data);
|
|
|
|
|
} else {
|
|
|
|
|
@@ -37,6 +41,31 @@ export function DigitalSignaturesModule() {
|
|
|
|
|
setEditingAsset(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
|
|
|
if (deletingId) {
|
|
|
|
|
await removeAsset(deletingId);
|
|
|
|
|
setDeletingId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddVersion = async (imageUrl: string, notes: string) => {
|
|
|
|
|
if (versionAsset) {
|
|
|
|
|
await addVersion(versionAsset.id, imageUrl, notes);
|
|
|
|
|
setVersionAsset(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isExpiringSoon = (date?: string) => {
|
|
|
|
|
if (!date) return false;
|
|
|
|
|
const diff = new Date(date).getTime() - Date.now();
|
|
|
|
|
return diff > 0 && diff < 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isExpired = (date?: string) => {
|
|
|
|
|
if (!date) return false;
|
|
|
|
|
return new Date(date).getTime() < Date.now();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Stats */}
|
|
|
|
|
@@ -79,14 +108,19 @@ export function DigitalSignaturesModule() {
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{assets.map((asset) => {
|
|
|
|
|
const Icon = TYPE_ICONS[asset.type];
|
|
|
|
|
const expired = isExpired(asset.expirationDate);
|
|
|
|
|
const expiringSoon = isExpiringSoon(asset.expirationDate);
|
|
|
|
|
return (
|
|
|
|
|
<Card key={asset.id} className="group relative">
|
|
|
|
|
<Card key={asset.id} className={`group relative ${expired ? 'border-destructive/50' : expiringSoon ? 'border-yellow-500/50' : ''}`}>
|
|
|
|
|
<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="Versiune nouă" onClick={() => setVersionAsset(asset)}>
|
|
|
|
|
<History className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingAsset(asset); setViewMode('edit'); }}>
|
|
|
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeAsset(asset.id)}>
|
|
|
|
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => setDeletingId(asset.id)}>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -99,14 +133,34 @@ export function DigitalSignaturesModule() {
|
|
|
|
|
<Icon className="h-6 w-6 text-muted-foreground" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<p className="font-medium">{asset.label}</p>
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
|
|
|
<Badge variant="outline" className="text-[10px]">{TYPE_LABELS[asset.type]}</Badge>
|
|
|
|
|
<span className="text-xs text-muted-foreground">{asset.owner}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Metadata row */}
|
|
|
|
|
<div className="mt-2 space-y-1">
|
|
|
|
|
{asset.legalStatus && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">Status legal: {asset.legalStatus}</p>
|
|
|
|
|
)}
|
|
|
|
|
{asset.expirationDate && (
|
|
|
|
|
<div className="flex items-center gap-1 text-xs">
|
|
|
|
|
{(expired || expiringSoon) && <AlertTriangle className="h-3 w-3 text-yellow-500" />}
|
|
|
|
|
<span className={expired ? 'text-destructive font-medium' : expiringSoon ? 'text-yellow-600 font-medium' : 'text-muted-foreground'}>
|
|
|
|
|
{expired ? 'Expirat' : expiringSoon ? 'Expiră curând' : 'Expiră'}: {asset.expirationDate}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{asset.usageNotes && (
|
|
|
|
|
<p className="text-xs text-muted-foreground line-clamp-1">Note: {asset.usageNotes}</p>
|
|
|
|
|
)}
|
|
|
|
|
{asset.versions.length > 0 && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">Versiuni: {asset.versions.length + 1}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
@@ -124,13 +178,74 @@ export function DigitalSignaturesModule() {
|
|
|
|
|
</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 element? Acțiunea este ireversibilă.</p>
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button variant="outline" onClick={() => setDeletingId(null)}>Anulează</Button>
|
|
|
|
|
<Button variant="destructive" onClick={handleDeleteConfirm}>Șterge</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{/* Add version dialog */}
|
|
|
|
|
<Dialog open={versionAsset !== null} onOpenChange={(open) => { if (!open) setVersionAsset(null); }}>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader><DialogTitle>Versiune nouă — {versionAsset?.label}</DialogTitle></DialogHeader>
|
|
|
|
|
<AddVersionForm
|
|
|
|
|
onSubmit={handleAddVersion}
|
|
|
|
|
onCancel={() => setVersionAsset(null)}
|
|
|
|
|
history={versionAsset?.versions ?? []}
|
|
|
|
|
/>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AddVersionForm({ onSubmit, onCancel, history }: {
|
|
|
|
|
onSubmit: (imageUrl: string, notes: string) => void;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
history: Array<{ id: string; imageUrl: string; notes: string; createdAt: string }>;
|
|
|
|
|
}) {
|
|
|
|
|
const [imageUrl, setImageUrl] = useState('');
|
|
|
|
|
const [notes, setNotes] = useState('');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{history.length > 0 && (
|
|
|
|
|
<div className="max-h-32 space-y-1 overflow-y-auto rounded border p-2">
|
|
|
|
|
<p className="text-xs font-medium text-muted-foreground">Istoric versiuni</p>
|
|
|
|
|
{history.map((v) => (
|
|
|
|
|
<div key={v.id} className="flex items-center justify-between text-xs">
|
|
|
|
|
<span className="truncate text-muted-foreground">{v.notes || 'Fără note'}</span>
|
|
|
|
|
<span className="shrink-0 text-muted-foreground">{v.createdAt.slice(0, 10)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div>
|
|
|
|
|
<Label>URL imagine nouă</Label>
|
|
|
|
|
<Input value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} className="mt-1" placeholder="https://... sau data:image/png;base64,..." required />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label>Note versiune</Label>
|
|
|
|
|
<Input value={notes} onChange={(e) => setNotes(e.target.value)} className="mt-1" placeholder="Ce s-a schimbat..." />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-end gap-2">
|
|
|
|
|
<Button variant="outline" onClick={onCancel}>Anulează</Button>
|
|
|
|
|
<Button onClick={() => { if (imageUrl.trim()) onSubmit(imageUrl, notes); }} disabled={!imageUrl.trim()}>Salvează versiune</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AssetForm({ initial, onSubmit, onCancel }: {
|
|
|
|
|
initial?: SignatureAsset;
|
|
|
|
|
onSubmit: (data: Omit<SignatureAsset, 'id' | 'createdAt'>) => void;
|
|
|
|
|
onSubmit: (data: Omit<SignatureAsset, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
const [label, setLabel] = useState(initial?.label ?? '');
|
|
|
|
|
@@ -138,11 +253,23 @@ function AssetForm({ initial, onSubmit, onCancel }: {
|
|
|
|
|
const [imageUrl, setImageUrl] = useState(initial?.imageUrl ?? '');
|
|
|
|
|
const [owner, setOwner] = useState(initial?.owner ?? '');
|
|
|
|
|
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
|
|
|
|
const [expirationDate, setExpirationDate] = useState(initial?.expirationDate ?? '');
|
|
|
|
|
const [legalStatus, setLegalStatus] = useState(initial?.legalStatus ?? '');
|
|
|
|
|
const [usageNotes, setUsageNotes] = useState(initial?.usageNotes ?? '');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ label, type, imageUrl, owner, company, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
|
|
|
|
|
<form onSubmit={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onSubmit({
|
|
|
|
|
label, type, imageUrl, owner, company,
|
|
|
|
|
expirationDate: expirationDate || undefined,
|
|
|
|
|
legalStatus, usageNotes,
|
|
|
|
|
versions: initial?.versions ?? [],
|
|
|
|
|
tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all',
|
|
|
|
|
});
|
|
|
|
|
}} className="space-y-4">
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
|
|
|
<div><Label>Denumire</Label><Input value={label} onChange={(e) => setLabel(e.target.value)} className="mt-1" required /></div>
|
|
|
|
|
<div><Label>Denumire *</Label><Input value={label} onChange={(e) => setLabel(e.target.value)} className="mt-1" required /></div>
|
|
|
|
|
<div><Label>Tip</Label>
|
|
|
|
|
<Select value={type} onValueChange={(v) => setType(v as SignatureAssetType)}>
|
|
|
|
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
|
|
|
|
@@ -173,6 +300,11 @@ function AssetForm({ initial, onSubmit, onCancel }: {
|
|
|
|
|
<Input value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} className="mt-1" placeholder="https://... sau data:image/png;base64,..." />
|
|
|
|
|
<p className="mt-1 text-xs text-muted-foreground">URL către imaginea semnăturii/ștampilei. Suportă URL-uri externe sau base64.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
|
|
|
<div><Label>Data expirare</Label><Input type="date" value={expirationDate} onChange={(e) => setExpirationDate(e.target.value)} className="mt-1" /></div>
|
|
|
|
|
<div><Label>Status legal</Label><Input value={legalStatus} onChange={(e) => setLegalStatus(e.target.value)} className="mt-1" placeholder="Valid, Anulat..." /></div>
|
|
|
|
|
<div><Label>Note utilizare</Label><Input value={usageNotes} onChange={(e) => setUsageNotes(e.target.value)} className="mt-1" placeholder="Doar pentru contracte..." /></div>
|
|
|
|
|
</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>
|
|
|
|
|
|