feat(registratura): add legal deadline tracking system (Termene Legale)
Full deadline tracking engine for Romanian construction permitting: - 16 deadline types across 5 categories (Avize, Completări, Analiză, Autorizare, Publicitate) - Working days vs calendar days with Romanian public holidays (Orthodox Easter via Meeus) - Backward deadlines (AC extension: 45 working days BEFORE expiry) - Chain deadlines (resolving one prompts adding the next) - Tacit approval auto-detection (overdue + applicable type) - Tabbed UI: Registru + Termene legale dashboard with stats/filters/table - Inline deadline cards in entry form with add/resolve/remove - Clock icon + count badge on registry table for entries with deadlines Also adds CLAUDE.md with full project context for AI assistant handoff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/shared/components/ui/dialog';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { DEADLINE_CATALOG, CATEGORY_LABELS } from '../services/deadline-catalog';
|
||||
import { computeDueDate } from '../services/working-days';
|
||||
import type { DeadlineCategory, DeadlineTypeDef } from '../types';
|
||||
|
||||
interface DeadlineAddDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
entryDate: string;
|
||||
onAdd: (typeId: string, startDate: string) => void;
|
||||
}
|
||||
|
||||
type Step = 'category' | 'type' | 'date';
|
||||
|
||||
const CATEGORIES: DeadlineCategory[] = ['avize', 'completari', 'analiza', 'autorizare', 'publicitate'];
|
||||
|
||||
export function DeadlineAddDialog({ open, onOpenChange, entryDate, onAdd }: DeadlineAddDialogProps) {
|
||||
const [step, setStep] = useState<Step>('category');
|
||||
const [selectedCategory, setSelectedCategory] = useState<DeadlineCategory | null>(null);
|
||||
const [selectedType, setSelectedType] = useState<DeadlineTypeDef | null>(null);
|
||||
const [startDate, setStartDate] = useState(entryDate);
|
||||
|
||||
const typesForCategory = useMemo(() => {
|
||||
if (!selectedCategory) return [];
|
||||
return DEADLINE_CATALOG.filter((d) => d.category === selectedCategory);
|
||||
}, [selectedCategory]);
|
||||
|
||||
const dueDatePreview = useMemo(() => {
|
||||
if (!selectedType || !startDate) return null;
|
||||
const start = new Date(startDate);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const due = computeDueDate(start, selectedType.days, selectedType.dayType, selectedType.isBackwardDeadline);
|
||||
return due.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}, [selectedType, startDate]);
|
||||
|
||||
const handleClose = () => {
|
||||
setStep('category');
|
||||
setSelectedCategory(null);
|
||||
setSelectedType(null);
|
||||
setStartDate(entryDate);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleCategorySelect = (cat: DeadlineCategory) => {
|
||||
setSelectedCategory(cat);
|
||||
setStep('type');
|
||||
};
|
||||
|
||||
const handleTypeSelect = (typ: DeadlineTypeDef) => {
|
||||
setSelectedType(typ);
|
||||
if (!typ.requiresCustomStartDate) {
|
||||
setStartDate(entryDate);
|
||||
}
|
||||
setStep('date');
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (step === 'type') {
|
||||
setStep('category');
|
||||
setSelectedCategory(null);
|
||||
} else if (step === 'date') {
|
||||
setStep('type');
|
||||
setSelectedType(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!selectedType || !startDate) return;
|
||||
onAdd(selectedType.id, startDate);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) handleClose(); }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 'category' && 'Adaugă termen legal — Categorie'}
|
||||
{step === 'type' && `Adaugă termen legal — ${selectedCategory ? CATEGORY_LABELS[selectedCategory] : ''}`}
|
||||
{step === 'date' && `Adaugă termen legal — ${selectedType?.label ?? ''}`}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{step === 'category' && (
|
||||
<div className="grid gap-2 py-2">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
className="flex items-center justify-between rounded-lg border p-3 text-left transition-colors hover:bg-accent"
|
||||
onClick={() => handleCategorySelect(cat)}
|
||||
>
|
||||
<span className="font-medium text-sm">{CATEGORY_LABELS[cat]}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{DEADLINE_CATALOG.filter((d) => d.category === cat).length}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'type' && (
|
||||
<div className="grid gap-2 py-2">
|
||||
{typesForCategory.map((typ) => (
|
||||
<button
|
||||
key={typ.id}
|
||||
type="button"
|
||||
className="rounded-lg border p-3 text-left transition-colors hover:bg-accent"
|
||||
onClick={() => handleTypeSelect(typ)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{typ.label}</span>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{typ.days} {typ.dayType === 'working' ? 'zile lucr.' : 'zile cal.'}
|
||||
</Badge>
|
||||
{typ.tacitApprovalApplicable && (
|
||||
<Badge variant="outline" className="text-[10px] text-blue-600">tacit</Badge>
|
||||
)}
|
||||
{typ.isBackwardDeadline && (
|
||||
<Badge variant="outline" className="text-[10px] text-orange-600">înapoi</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{typ.description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'date' && selectedType && (
|
||||
<div className="space-y-4 py-2">
|
||||
<div>
|
||||
<Label>{selectedType.startDateLabel}</Label>
|
||||
{selectedType.startDateHint && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{selectedType.startDateHint}</p>
|
||||
)}
|
||||
<Input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{dueDatePreview && (
|
||||
<div className="rounded-lg border bg-muted/30 p-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedType.isBackwardDeadline ? 'Termen limită depunere' : 'Termen limită calculat'}
|
||||
</p>
|
||||
<p className="text-lg font-bold">{dueDatePreview}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{selectedType.days} {selectedType.dayType === 'working' ? 'zile lucrătoare' : 'zile calendaristice'}
|
||||
{selectedType.isBackwardDeadline ? ' ÎNAINTE' : ' de la data start'}
|
||||
</p>
|
||||
{selectedType.legalReference && (
|
||||
<p className="text-xs text-muted-foreground mt-1 italic">Ref: {selectedType.legalReference}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
{step !== 'category' && (
|
||||
<Button type="button" variant="outline" onClick={handleBack}>Înapoi</Button>
|
||||
)}
|
||||
{step === 'category' && (
|
||||
<Button type="button" variant="outline" onClick={handleClose}>Anulează</Button>
|
||||
)}
|
||||
{step === 'date' && (
|
||||
<Button type="button" onClick={handleConfirm} disabled={!startDate}>
|
||||
Adaugă termen
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { Clock, CheckCircle2, X } from 'lucide-react';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import type { TrackedDeadline } from '../types';
|
||||
import { getDeadlineType } from '../services/deadline-catalog';
|
||||
import { getDeadlineDisplayStatus } from '../services/deadline-service';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
|
||||
interface DeadlineCardProps {
|
||||
deadline: TrackedDeadline;
|
||||
onResolve: (deadline: TrackedDeadline) => void;
|
||||
onRemove: (deadlineId: string) => void;
|
||||
}
|
||||
|
||||
const VARIANT_CLASSES: Record<string, string> = {
|
||||
green: 'border-green-500/30 bg-green-50 dark:bg-green-950/20',
|
||||
yellow: 'border-yellow-500/30 bg-yellow-50 dark:bg-yellow-950/20',
|
||||
red: 'border-red-500/30 bg-red-50 dark:bg-red-950/20',
|
||||
blue: 'border-blue-500/30 bg-blue-50 dark:bg-blue-950/20',
|
||||
gray: 'border-muted bg-muted/30',
|
||||
};
|
||||
|
||||
const BADGE_CLASSES: Record<string, string> = {
|
||||
green: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
yellow: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
red: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
blue: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
gray: 'bg-muted text-muted-foreground',
|
||||
};
|
||||
|
||||
export function DeadlineCard({ deadline, onResolve, onRemove }: DeadlineCardProps) {
|
||||
const def = getDeadlineType(deadline.typeId);
|
||||
const status = getDeadlineDisplayStatus(deadline);
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-3 rounded-lg border p-3', VARIANT_CLASSES[status.variant] ?? '')}>
|
||||
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{def?.label ?? deadline.typeId}</span>
|
||||
<Badge className={cn('text-[10px] border-0', BADGE_CLASSES[status.variant] ?? '')}>
|
||||
{status.label}
|
||||
{status.daysRemaining !== null && status.variant !== 'blue' && (
|
||||
<span className="ml-1">
|
||||
({status.daysRemaining < 0 ? `${Math.abs(status.daysRemaining)}z depășit` : `${status.daysRemaining}z`})
|
||||
</span>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
{def?.isBackwardDeadline ? 'Termen limită' : 'Start'}: {formatDate(deadline.startDate)}
|
||||
{' → '}
|
||||
{def?.isBackwardDeadline ? 'Depunere până la' : 'Termen'}: {formatDate(deadline.dueDate)}
|
||||
{def?.dayType === 'working' && <span className="ml-1">(zile lucrătoare)</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
{deadline.resolution === 'pending' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-green-600"
|
||||
onClick={() => onResolve(deadline)}
|
||||
title="Rezolvă"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={() => onRemove(deadline.id)}
|
||||
title="Șterge"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardContent } from '@/shared/components/ui/card';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import type { RegistryEntry, TrackedDeadline, DeadlineResolution, DeadlineCategory } from '../types';
|
||||
import { aggregateDeadlines } from '../services/deadline-service';
|
||||
import { CATEGORY_LABELS, getDeadlineType } from '../services/deadline-catalog';
|
||||
import { useDeadlineFilters } from '../hooks/use-deadline-filters';
|
||||
import { DeadlineTable } from './deadline-table';
|
||||
import { DeadlineResolveDialog } from './deadline-resolve-dialog';
|
||||
|
||||
interface DeadlineDashboardProps {
|
||||
entries: RegistryEntry[];
|
||||
onResolveDeadline: (entryId: string, deadlineId: string, resolution: DeadlineResolution, note: string, chainNext: boolean) => void;
|
||||
onAddChainedDeadline: (entryId: string, typeId: string, startDate: string, parentId: string) => void;
|
||||
}
|
||||
|
||||
const RESOLUTION_LABELS: Record<string, string> = {
|
||||
pending: 'În așteptare',
|
||||
completed: 'Finalizat',
|
||||
'aprobat-tacit': 'Aprobat tacit',
|
||||
respins: 'Respins',
|
||||
anulat: 'Anulat',
|
||||
};
|
||||
|
||||
export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDeadline }: DeadlineDashboardProps) {
|
||||
const { filters, updateFilter } = useDeadlineFilters();
|
||||
const [resolvingEntry, setResolvingEntry] = useState<string | null>(null);
|
||||
const [resolvingDeadline, setResolvingDeadline] = useState<TrackedDeadline | null>(null);
|
||||
|
||||
const stats = useMemo(() => aggregateDeadlines(entries), [entries]);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
return stats.all.filter((row) => {
|
||||
if (filters.category !== 'all') {
|
||||
const def = getDeadlineType(row.deadline.typeId);
|
||||
if (def && def.category !== filters.category) return false;
|
||||
}
|
||||
if (filters.resolution !== 'all') {
|
||||
// Map tacit display status to actual resolution filter
|
||||
if (filters.resolution === 'pending') {
|
||||
if (row.deadline.resolution !== 'pending') return false;
|
||||
} else if (row.deadline.resolution !== filters.resolution) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.urgentOnly) {
|
||||
if (row.status.variant !== 'yellow' && row.status.variant !== 'red') return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [stats.all, filters]);
|
||||
|
||||
const handleResolveClick = (entryId: string, deadline: TrackedDeadline) => {
|
||||
setResolvingEntry(entryId);
|
||||
setResolvingDeadline(deadline);
|
||||
};
|
||||
|
||||
const handleResolve = (resolution: DeadlineResolution, note: string, chainNext: boolean) => {
|
||||
if (!resolvingEntry || !resolvingDeadline) return;
|
||||
onResolveDeadline(resolvingEntry, resolvingDeadline.id, resolution, note, chainNext);
|
||||
|
||||
// Handle chain creation
|
||||
if (chainNext) {
|
||||
const def = getDeadlineType(resolvingDeadline.typeId);
|
||||
if (def?.chainNextTypeId) {
|
||||
const resolvedDate = new Date().toISOString().slice(0, 10);
|
||||
onAddChainedDeadline(resolvingEntry, def.chainNextTypeId, resolvedDate, resolvingDeadline.id);
|
||||
}
|
||||
}
|
||||
|
||||
setResolvingEntry(null);
|
||||
setResolvingDeadline(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Active" value={stats.active} />
|
||||
<StatCard label="Urgente" value={stats.urgent} variant={stats.urgent > 0 ? 'destructive' : undefined} />
|
||||
<StatCard label="Depășit termen" value={stats.overdue} variant={stats.overdue > 0 ? 'destructive' : undefined} />
|
||||
<StatCard label="Aprobat tacit" value={stats.tacit} variant={stats.tacit > 0 ? 'blue' : undefined} />
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">Categorie</Label>
|
||||
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v as DeadlineCategory | 'all')}>
|
||||
<SelectTrigger className="mt-1 w-[160px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toate</SelectItem>
|
||||
{(Object.entries(CATEGORY_LABELS) as [DeadlineCategory, string][]).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">Status</Label>
|
||||
<Select value={filters.resolution} onValueChange={(v) => updateFilter('resolution', v as DeadlineResolution | 'all')}>
|
||||
<SelectTrigger className="mt-1 w-[160px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toate</SelectItem>
|
||||
{Object.entries(RESOLUTION_LABELS).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant={filters.urgentOnly ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => updateFilter('urgentOnly', !filters.urgentOnly)}
|
||||
>
|
||||
Doar urgente
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<DeadlineTable rows={filteredRows} onResolve={handleResolveClick} />
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{filteredRows.length} din {stats.all.length} termene afișate
|
||||
</p>
|
||||
|
||||
<DeadlineResolveDialog
|
||||
open={resolvingDeadline !== null}
|
||||
deadline={resolvingDeadline}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setResolvingEntry(null);
|
||||
setResolvingDeadline(null);
|
||||
}
|
||||
}}
|
||||
onResolve={handleResolve}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' | 'blue' }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className={`text-2xl font-bold ${
|
||||
variant === 'destructive' && value > 0 ? 'text-destructive' : ''
|
||||
}${variant === 'blue' && value > 0 ? 'text-blue-600' : ''}`}>
|
||||
{value}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/shared/components/ui/dialog';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Textarea } from '@/shared/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
||||
import type { TrackedDeadline, DeadlineResolution } from '../types';
|
||||
import { getDeadlineType } from '../services/deadline-catalog';
|
||||
|
||||
interface DeadlineResolveDialogProps {
|
||||
open: boolean;
|
||||
deadline: TrackedDeadline | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onResolve: (resolution: DeadlineResolution, note: string, chainNext: boolean) => void;
|
||||
}
|
||||
|
||||
const RESOLUTION_OPTIONS: Array<{ value: DeadlineResolution; label: string }> = [
|
||||
{ value: 'completed', label: 'Finalizat' },
|
||||
{ value: 'aprobat-tacit', label: 'Aprobat tacit' },
|
||||
{ value: 'respins', label: 'Respins' },
|
||||
{ value: 'anulat', label: 'Anulat' },
|
||||
];
|
||||
|
||||
export function DeadlineResolveDialog({ open, deadline, onOpenChange, onResolve }: DeadlineResolveDialogProps) {
|
||||
const [resolution, setResolution] = useState<DeadlineResolution>('completed');
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
if (!deadline) return null;
|
||||
|
||||
const def = getDeadlineType(deadline.typeId);
|
||||
const hasChain = def?.chainNextTypeId && (resolution === 'completed' || resolution === 'aprobat-tacit');
|
||||
|
||||
const handleResolve = () => {
|
||||
onResolve(resolution, note, !!hasChain);
|
||||
setResolution('completed');
|
||||
setNote('');
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setResolution('completed');
|
||||
setNote('');
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) handleClose(); }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rezolvă — {def?.label ?? deadline.typeId}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div>
|
||||
<Label>Rezoluție</Label>
|
||||
<Select value={resolution} onValueChange={(v) => setResolution(v as DeadlineResolution)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{RESOLUTION_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Notă (opțional)</Label>
|
||||
<Textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
rows={2}
|
||||
className="mt-1"
|
||||
placeholder="Detalii rezoluție..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasChain && def?.chainNextActionLabel && (
|
||||
<div className="rounded-lg border border-blue-500/30 bg-blue-50 dark:bg-blue-950/20 p-3">
|
||||
<p className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Termen înlănțuit disponibil
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
La rezolvare veți fi întrebat dacă doriți: {def.chainNextActionLabel}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={handleClose}>Anulează</Button>
|
||||
<Button type="button" onClick={handleResolve}>Rezolvă</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import { CheckCircle2 } from 'lucide-react';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import type { TrackedDeadline, RegistryEntry } from '../types';
|
||||
import type { DeadlineDisplayStatus } from '../services/deadline-service';
|
||||
import { getDeadlineType, CATEGORY_LABELS } from '../services/deadline-catalog';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
|
||||
interface DeadlineRow {
|
||||
deadline: TrackedDeadline;
|
||||
entry: RegistryEntry;
|
||||
status: DeadlineDisplayStatus;
|
||||
}
|
||||
|
||||
interface DeadlineTableProps {
|
||||
rows: DeadlineRow[];
|
||||
onResolve: (entryId: string, deadline: TrackedDeadline) => void;
|
||||
}
|
||||
|
||||
const BADGE_CLASSES: Record<string, string> = {
|
||||
green: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 border-0',
|
||||
yellow: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border-0',
|
||||
red: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 border-0',
|
||||
blue: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 border-0',
|
||||
gray: 'bg-muted text-muted-foreground border-0',
|
||||
};
|
||||
|
||||
const ROW_CLASSES: Record<string, string> = {
|
||||
red: 'bg-destructive/5',
|
||||
yellow: 'bg-yellow-50/50 dark:bg-yellow-950/10',
|
||||
blue: 'bg-blue-50/50 dark:bg-blue-950/10',
|
||||
};
|
||||
|
||||
export function DeadlineTable({ rows, onResolve }: DeadlineTableProps) {
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
Niciun termen legal urmărit.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40">
|
||||
<th className="px-3 py-2 text-left font-medium">Nr. înreg.</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Categorie</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Tip termen</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Data start</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Termen limită</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Zile</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Status</th>
|
||||
<th className="px-3 py-2 text-right font-medium">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => {
|
||||
const def = getDeadlineType(row.deadline.typeId);
|
||||
return (
|
||||
<tr
|
||||
key={row.deadline.id}
|
||||
className={cn(
|
||||
'border-b transition-colors hover:bg-muted/20',
|
||||
ROW_CLASSES[row.status.variant] ?? '',
|
||||
)}
|
||||
>
|
||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{row.entry.number}</td>
|
||||
<td className="px-3 py-2 text-xs">
|
||||
{def ? CATEGORY_LABELS[def.category] : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs">
|
||||
<span className="font-medium">{def?.label ?? row.deadline.typeId}</span>
|
||||
{def?.dayType === 'working' && (
|
||||
<span className="ml-1 text-muted-foreground">(lucr.)</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs whitespace-nowrap">{formatDate(row.deadline.startDate)}</td>
|
||||
<td className="px-3 py-2 text-xs whitespace-nowrap font-medium">{formatDate(row.deadline.dueDate)}</td>
|
||||
<td className="px-3 py-2 text-xs whitespace-nowrap">
|
||||
{row.status.daysRemaining !== null ? (
|
||||
<span className={cn(row.status.daysRemaining < 0 && 'text-destructive font-medium')}>
|
||||
{row.status.daysRemaining < 0
|
||||
? `${Math.abs(row.status.daysRemaining)}z depășit`
|
||||
: `${row.status.daysRemaining}z`}
|
||||
</span>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge className={cn('text-[10px]', BADGE_CLASSES[row.status.variant] ?? '')}>
|
||||
{row.status.label}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
{row.deadline.resolution === 'pending' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-green-600"
|
||||
onClick={() => onResolve(row.entry.id, row.deadline)}
|
||||
title="Rezolvă"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/shared/components/ui/dialog';
|
||||
@@ -12,8 +13,10 @@ import { useRegistry } from '../hooks/use-registry';
|
||||
import { RegistryFilters } from './registry-filters';
|
||||
import { RegistryTable } from './registry-table';
|
||||
import { RegistryEntryForm } from './registry-entry-form';
|
||||
import { DeadlineDashboard } from './deadline-dashboard';
|
||||
import { getOverdueDays } from '../services/registry-service';
|
||||
import type { RegistryEntry } from '../types';
|
||||
import { aggregateDeadlines } from '../services/deadline-service';
|
||||
import type { RegistryEntry, DeadlineResolution } from '../types';
|
||||
|
||||
type ViewMode = 'list' | 'add' | 'edit';
|
||||
|
||||
@@ -21,6 +24,7 @@ export function RegistraturaModule() {
|
||||
const {
|
||||
entries, allEntries, loading, filters, updateFilter,
|
||||
addEntry, updateEntry, removeEntry, closeEntry,
|
||||
addDeadline, resolveDeadline, removeDeadline,
|
||||
} = useRegistry();
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
@@ -69,6 +73,21 @@ export function RegistraturaModule() {
|
||||
setEditingEntry(null);
|
||||
};
|
||||
|
||||
// ── Dashboard deadline resolve/chain handlers ──
|
||||
const handleDashboardResolve = async (
|
||||
entryId: string,
|
||||
deadlineId: string,
|
||||
resolution: DeadlineResolution,
|
||||
note: string,
|
||||
_chainNext: boolean,
|
||||
) => {
|
||||
await resolveDeadline(entryId, deadlineId, resolution, note);
|
||||
};
|
||||
|
||||
const handleAddChainedDeadline = async (entryId: string, typeId: string, startDate: string, parentId: string) => {
|
||||
await addDeadline(entryId, typeId, startDate, parentId);
|
||||
};
|
||||
|
||||
// Stats
|
||||
const total = allEntries.length;
|
||||
const open = allEntries.filter((e) => e.status === 'deschis').length;
|
||||
@@ -79,101 +98,128 @@ export function RegistraturaModule() {
|
||||
}).length;
|
||||
const intrat = allEntries.filter((e) => e.direction === 'intrat').length;
|
||||
|
||||
const deadlineStats = useMemo(() => aggregateDeadlines(allEntries), [allEntries]);
|
||||
const urgentDeadlines = deadlineStats.urgent + deadlineStats.overdue;
|
||||
|
||||
const closingEntry = closingId ? allEntries.find((e) => e.id === closingId) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Total" value={total} />
|
||||
<StatCard label="Deschise" value={open} />
|
||||
<StatCard label="Depășite" value={overdue} variant={overdue > 0 ? 'destructive' : undefined} />
|
||||
<StatCard label="Intrate" value={intrat} />
|
||||
</div>
|
||||
|
||||
{viewMode === 'list' && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<RegistryFilters filters={filters} onUpdate={updateFilter} />
|
||||
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
||||
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<RegistryTable
|
||||
entries={entries}
|
||||
loading={loading}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onClose={handleCloseRequest}
|
||||
/>
|
||||
|
||||
{!loading && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{entries.length} din {total} înregistrări afișate
|
||||
</p>
|
||||
<Tabs defaultValue="registru">
|
||||
<TabsList>
|
||||
<TabsTrigger value="registru">Registru</TabsTrigger>
|
||||
<TabsTrigger value="termene">
|
||||
Termene legale
|
||||
{urgentDeadlines > 0 && (
|
||||
<Badge variant="destructive" className="ml-1.5 text-[10px] px-1.5 py-0">
|
||||
{urgentDeadlines}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{viewMode === 'add' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Înregistrare nouă
|
||||
<Badge variant="outline" className="text-xs">Nr. auto</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RegistryEntryForm
|
||||
allEntries={allEntries}
|
||||
onSubmit={handleAdd}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{viewMode === 'edit' && editingEntry && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Editare — {editingEntry.number}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RegistryEntryForm
|
||||
initial={editingEntry}
|
||||
allEntries={allEntries}
|
||||
onSubmit={handleUpdate}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Close confirmation dialog */}
|
||||
<Dialog open={closingId !== null} onOpenChange={(open) => { if (!open) setClosingId(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Închide înregistrarea</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
<p className="text-sm">
|
||||
Această înregistrare are {closingEntry?.linkedEntryIds?.length ?? 0} înregistrări legate.
|
||||
Vrei să le închizi și pe acestea?
|
||||
</p>
|
||||
<TabsContent value="registru">
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Total" value={total} />
|
||||
<StatCard label="Deschise" value={open} />
|
||||
<StatCard label="Depășite" value={overdue} variant={overdue > 0 ? 'destructive' : undefined} />
|
||||
<StatCard label="Intrate" value={intrat} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setClosingId(null)}>Anulează</Button>
|
||||
<Button variant="secondary" onClick={() => handleCloseConfirm(false)}>
|
||||
Doar aceasta
|
||||
</Button>
|
||||
<Button onClick={() => handleCloseConfirm(true)}>
|
||||
Închide toate legate
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{viewMode === 'list' && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<RegistryFilters filters={filters} onUpdate={updateFilter} />
|
||||
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
||||
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<RegistryTable
|
||||
entries={entries}
|
||||
loading={loading}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onClose={handleCloseRequest}
|
||||
/>
|
||||
|
||||
{!loading && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{entries.length} din {total} înregistrări afișate
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{viewMode === 'add' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Înregistrare nouă
|
||||
<Badge variant="outline" className="text-xs">Nr. auto</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RegistryEntryForm
|
||||
allEntries={allEntries}
|
||||
onSubmit={handleAdd}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{viewMode === 'edit' && editingEntry && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Editare — {editingEntry.number}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RegistryEntryForm
|
||||
initial={editingEntry}
|
||||
allEntries={allEntries}
|
||||
onSubmit={handleUpdate}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Close confirmation dialog */}
|
||||
<Dialog open={closingId !== null} onOpenChange={(open) => { if (!open) setClosingId(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Închide înregistrarea</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
<p className="text-sm">
|
||||
Această înregistrare are {closingEntry?.linkedEntryIds?.length ?? 0} înregistrări legate.
|
||||
Vrei să le închizi și pe acestea?
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setClosingId(null)}>Anulează</Button>
|
||||
<Button variant="secondary" onClick={() => handleCloseConfirm(false)}>
|
||||
Doar aceasta
|
||||
</Button>
|
||||
<Button onClick={() => handleCloseConfirm(true)}>
|
||||
Închide toate legate
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="termene">
|
||||
<DeadlineDashboard
|
||||
entries={allEntries}
|
||||
onResolveDeadline={handleDashboardResolve}
|
||||
onAddChainedDeadline={handleAddChainedDeadline}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import { Paperclip, X } from 'lucide-react';
|
||||
import { Paperclip, X, Clock, Plus } from 'lucide-react';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, RegistryAttachment } from '../types';
|
||||
import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, RegistryAttachment, TrackedDeadline, DeadlineResolution } from '../types';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Textarea } from '@/shared/components/ui/textarea';
|
||||
@@ -12,6 +12,11 @@ import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
||||
import { useContacts } from '@/modules/address-book/hooks/use-contacts';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DeadlineCard } from './deadline-card';
|
||||
import { DeadlineAddDialog } from './deadline-add-dialog';
|
||||
import { DeadlineResolveDialog } from './deadline-resolve-dialog';
|
||||
import { createTrackedDeadline, resolveDeadline as resolveDeadlineFn } from '../services/deadline-service';
|
||||
import { getDeadlineType } from '../services/deadline-catalog';
|
||||
|
||||
interface RegistryEntryFormProps {
|
||||
initial?: RegistryEntry;
|
||||
@@ -50,6 +55,39 @@ export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: R
|
||||
const [notes, setNotes] = useState(initial?.notes ?? '');
|
||||
const [linkedEntryIds, setLinkedEntryIds] = useState<string[]>(initial?.linkedEntryIds ?? []);
|
||||
const [attachments, setAttachments] = useState<RegistryAttachment[]>(initial?.attachments ?? []);
|
||||
const [trackedDeadlines, setTrackedDeadlines] = useState<TrackedDeadline[]>(initial?.trackedDeadlines ?? []);
|
||||
|
||||
// ── Deadline dialogs ──
|
||||
const [deadlineAddOpen, setDeadlineAddOpen] = useState(false);
|
||||
const [resolvingDeadline, setResolvingDeadline] = useState<TrackedDeadline | null>(null);
|
||||
|
||||
const handleAddDeadline = (typeId: string, startDate: string, chainParentId?: string) => {
|
||||
const tracked = createTrackedDeadline(typeId, startDate, chainParentId);
|
||||
if (tracked) setTrackedDeadlines((prev) => [...prev, tracked]);
|
||||
};
|
||||
|
||||
const handleResolveDeadline = (resolution: DeadlineResolution, note: string, chainNext: boolean) => {
|
||||
if (!resolvingDeadline) return;
|
||||
const resolved = resolveDeadlineFn(resolvingDeadline, resolution, note);
|
||||
setTrackedDeadlines((prev) =>
|
||||
prev.map((d) => (d.id === resolved.id ? resolved : d))
|
||||
);
|
||||
|
||||
// Handle chain
|
||||
if (chainNext) {
|
||||
const def = getDeadlineType(resolvingDeadline.typeId);
|
||||
if (def?.chainNextTypeId) {
|
||||
const resolvedDate = new Date().toISOString().slice(0, 10);
|
||||
const chained = createTrackedDeadline(def.chainNextTypeId, resolvedDate, resolvingDeadline.id);
|
||||
if (chained) setTrackedDeadlines((prev) => [...prev, chained]);
|
||||
}
|
||||
}
|
||||
setResolvingDeadline(null);
|
||||
};
|
||||
|
||||
const handleRemoveDeadline = (deadlineId: string) => {
|
||||
setTrackedDeadlines((prev) => prev.filter((d) => d.id !== deadlineId));
|
||||
};
|
||||
|
||||
// ── Sender/Recipient autocomplete suggestions ──
|
||||
const [senderFocused, setSenderFocused] = useState(false);
|
||||
@@ -111,6 +149,7 @@ export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: R
|
||||
deadline: deadline || undefined,
|
||||
linkedEntryIds,
|
||||
attachments,
|
||||
trackedDeadlines: trackedDeadlines.length > 0 ? trackedDeadlines : undefined,
|
||||
notes,
|
||||
tags: initial?.tags ?? [],
|
||||
visibility: initial?.visibility ?? 'all',
|
||||
@@ -278,6 +317,50 @@ export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: R
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tracked Deadlines */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Termene legale
|
||||
</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setDeadlineAddOpen(true)}>
|
||||
<Plus className="mr-1 h-3.5 w-3.5" /> Adaugă termen
|
||||
</Button>
|
||||
</div>
|
||||
{trackedDeadlines.length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{trackedDeadlines.map((dl) => (
|
||||
<DeadlineCard
|
||||
key={dl.id}
|
||||
deadline={dl}
|
||||
onResolve={setResolvingDeadline}
|
||||
onRemove={handleRemoveDeadline}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{trackedDeadlines.length === 0 && (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Niciun termen legal. Apăsați "Adaugă termen" pentru a urmări un termen.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeadlineAddDialog
|
||||
open={deadlineAddOpen}
|
||||
onOpenChange={setDeadlineAddOpen}
|
||||
entryDate={date}
|
||||
onAdd={handleAddDeadline}
|
||||
/>
|
||||
|
||||
<DeadlineResolveDialog
|
||||
open={resolvingDeadline !== null}
|
||||
deadline={resolvingDeadline}
|
||||
onOpenChange={(open) => { if (!open) setResolvingDeadline(null); }}
|
||||
onResolve={handleResolveDeadline}
|
||||
/>
|
||||
|
||||
{/* Attachments */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Pencil, Trash2, CheckCircle2, Link2 } from 'lucide-react';
|
||||
import { Pencil, Trash2, CheckCircle2, Link2, Clock } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import type { RegistryEntry, DocumentType } from '../types';
|
||||
@@ -100,6 +100,12 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
|
||||
{entry.attachments.length} fișiere
|
||||
</Badge>
|
||||
)}
|
||||
{(entry.trackedDeadlines ?? []).length > 0 && (
|
||||
<Badge variant="outline" className="ml-1 text-[10px] px-1">
|
||||
<Clock className="mr-0.5 inline h-2.5 w-2.5" />
|
||||
{(entry.trackedDeadlines ?? []).length}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 max-w-[130px] truncate">{entry.sender}</td>
|
||||
<td className="px-3 py-2 max-w-[130px] truncate">{entry.recipient}</td>
|
||||
|
||||
Reference in New Issue
Block a user