From 455d95a8c6ed0b3c0431c9d1b3eb94d289b95340 Mon Sep 17 00:00:00 2001 From: Marius Tarau Date: Wed, 18 Feb 2026 06:35:42 +0200 Subject: [PATCH] feat(digital-signatures): add version history, expiration tracking, and metadata fields - Version history with add-version dialog and history display - Expiration date with expired/expiring-soon visual indicators - Legal status and usage notes fields - Delete confirmation dialog - updatedAt timestamp support - Image preview in version history Co-Authored-By: Claude Opus 4.6 --- .../components/digital-signatures-module.tsx | 152 ++++++++++++++++-- .../hooks/use-signatures.ts | 23 ++- src/modules/digital-signatures/index.ts | 2 +- src/modules/digital-signatures/types.ts | 17 ++ 4 files changed, 178 insertions(+), 16 deletions(-) diff --git a/src/modules/digital-signatures/components/digital-signatures-module.tsx b/src/modules/digital-signatures/components/digital-signatures-module.tsx index 946cfdd..fea5988 100644 --- a/src/modules/digital-signatures/components/digital-signatures-module.tsx +++ b/src/modules/digital-signatures/components/digital-signatures-module.tsx @@ -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 = { 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('list'); const [editingAsset, setEditingAsset] = useState(null); + const [deletingId, setDeletingId] = useState(null); + const [versionAsset, setVersionAsset] = useState(null); - const handleSubmit = async (data: Omit) => { + const handleSubmit = async (data: Omit) => { 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 (
{/* Stats */} @@ -79,14 +108,19 @@ export function DigitalSignaturesModule() {
{assets.map((asset) => { const Icon = TYPE_ICONS[asset.type]; + const expired = isExpired(asset.expirationDate); + const expiringSoon = isExpiringSoon(asset.expirationDate); return ( - +
+ -
@@ -99,14 +133,34 @@ export function DigitalSignaturesModule() { )}
-
+

{asset.label}

-
+
{TYPE_LABELS[asset.type]} {asset.owner}
+ {/* Metadata row */} +
+ {asset.legalStatus && ( +

Status legal: {asset.legalStatus}

+ )} + {asset.expirationDate && ( +
+ {(expired || expiringSoon) && } + + {expired ? 'Expirat' : expiringSoon ? 'Expiră curând' : 'Expiră'}: {asset.expirationDate} + +
+ )} + {asset.usageNotes && ( +

Note: {asset.usageNotes}

+ )} + {asset.versions.length > 0 && ( +

Versiuni: {asset.versions.length + 1}

+ )} +
); @@ -124,13 +178,74 @@ export function DigitalSignaturesModule() { )} + + {/* Delete confirmation */} + { if (!open) setDeletingId(null); }}> + + Confirmare ștergere +

Ești sigur că vrei să ștergi acest element? Acțiunea este ireversibilă.

+ + + + +
+
+ + {/* Add version dialog */} + { if (!open) setVersionAsset(null); }}> + + Versiune nouă — {versionAsset?.label} + setVersionAsset(null)} + history={versionAsset?.versions ?? []} + /> + + +
+ ); +} + +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 ( +
+ {history.length > 0 && ( +
+

Istoric versiuni

+ {history.map((v) => ( +
+ {v.notes || 'Fără note'} + {v.createdAt.slice(0, 10)} +
+ ))} +
+ )} +
+ + setImageUrl(e.target.value)} className="mt-1" placeholder="https://... sau data:image/png;base64,..." required /> +
+
+ + setNotes(e.target.value)} className="mt-1" placeholder="Ce s-a schimbat..." /> +
+
+ + +
); } function AssetForm({ initial, onSubmit, onCancel }: { initial?: SignatureAsset; - onSubmit: (data: Omit) => void; + onSubmit: (data: Omit) => 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(initial?.company ?? 'beletage'); + const [expirationDate, setExpirationDate] = useState(initial?.expirationDate ?? ''); + const [legalStatus, setLegalStatus] = useState(initial?.legalStatus ?? ''); + const [usageNotes, setUsageNotes] = useState(initial?.usageNotes ?? ''); return ( -
{ e.preventDefault(); onSubmit({ label, type, imageUrl, owner, company, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4"> + { + 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">
-
setLabel(e.target.value)} className="mt-1" required />
+
setLabel(e.target.value)} className="mt-1" required />
setImageUrl(e.target.value)} className="mt-1" placeholder="https://... sau data:image/png;base64,..." />

URL către imaginea semnăturii/ștampilei. Suportă URL-uri externe sau base64.

+
+
setExpirationDate(e.target.value)} className="mt-1" />
+
setLegalStatus(e.target.value)} className="mt-1" placeholder="Valid, Anulat..." />
+
setUsageNotes(e.target.value)} className="mt-1" placeholder="Doar pentru contracte..." />
+
diff --git a/src/modules/digital-signatures/hooks/use-signatures.ts b/src/modules/digital-signatures/hooks/use-signatures.ts index a4194f5..c4a32e7 100644 --- a/src/modules/digital-signatures/hooks/use-signatures.ts +++ b/src/modules/digital-signatures/hooks/use-signatures.ts @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useStorage } from '@/core/storage'; import { v4 as uuid } from 'uuid'; -import type { SignatureAsset, SignatureAssetType } from '../types'; +import type { SignatureAsset, SignatureAssetType, AssetVersion } from '../types'; const PREFIX = 'sig:'; @@ -36,8 +36,9 @@ export function useSignatures() { // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { refresh(); }, [refresh]); - const addAsset = useCallback(async (data: Omit) => { - const asset: SignatureAsset = { ...data, id: uuid(), createdAt: new Date().toISOString() }; + const addAsset = useCallback(async (data: Omit) => { + const now = new Date().toISOString(); + const asset: SignatureAsset = { ...data, id: uuid(), createdAt: now, updatedAt: now }; await storage.set(`${PREFIX}${asset.id}`, asset); await refresh(); return asset; @@ -46,11 +47,23 @@ export function useSignatures() { const updateAsset = useCallback(async (id: string, updates: Partial) => { const existing = assets.find((a) => a.id === id); if (!existing) return; - const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt }; + const updated: SignatureAsset = { + ...existing, ...updates, + id: existing.id, createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; await storage.set(`${PREFIX}${id}`, updated); await refresh(); }, [storage, refresh, assets]); + const addVersion = useCallback(async (assetId: string, imageUrl: string, notes: string) => { + const existing = assets.find((a) => a.id === assetId); + if (!existing) return; + const version: AssetVersion = { id: uuid(), imageUrl, notes, createdAt: new Date().toISOString() }; + const updatedVersions = [...existing.versions, version]; + await updateAsset(assetId, { imageUrl, versions: updatedVersions }); + }, [assets, updateAsset]); + const removeAsset = useCallback(async (id: string) => { await storage.delete(`${PREFIX}${id}`); await refresh(); @@ -69,5 +82,5 @@ export function useSignatures() { return true; }); - return { assets: filteredAssets, allAssets: assets, loading, filters, updateFilter, addAsset, updateAsset, removeAsset, refresh }; + return { assets: filteredAssets, allAssets: assets, loading, filters, updateFilter, addAsset, updateAsset, addVersion, removeAsset, refresh }; } diff --git a/src/modules/digital-signatures/index.ts b/src/modules/digital-signatures/index.ts index 38a3d4d..7916f2b 100644 --- a/src/modules/digital-signatures/index.ts +++ b/src/modules/digital-signatures/index.ts @@ -1,3 +1,3 @@ export { digitalSignaturesConfig } from './config'; export { DigitalSignaturesModule } from './components/digital-signatures-module'; -export type { SignatureAsset, SignatureAssetType } from './types'; +export type { SignatureAsset, SignatureAssetType, AssetVersion } from './types'; diff --git a/src/modules/digital-signatures/types.ts b/src/modules/digital-signatures/types.ts index 6b7c67e..423a3f5 100644 --- a/src/modules/digital-signatures/types.ts +++ b/src/modules/digital-signatures/types.ts @@ -3,6 +3,14 @@ import type { CompanyId } from '@/core/auth/types'; export type SignatureAssetType = 'signature' | 'stamp' | 'initials'; +/** Version history entry */ +export interface AssetVersion { + id: string; + imageUrl: string; + notes: string; + createdAt: string; +} + export interface SignatureAsset { id: string; label: string; @@ -10,7 +18,16 @@ export interface SignatureAsset { imageUrl: string; owner: string; company: CompanyId; + /** Expiration date (YYYY-MM-DD) */ + expirationDate?: string; + /** Legal status description */ + legalStatus: string; + /** Usage notes */ + usageNotes: string; + /** Version history */ + versions: AssetVersion[]; tags: string[]; visibility: Visibility; createdAt: string; + updatedAt: string; }