3.03 Registratura Termene Legale recipient registration, audit log, expiry tracking
- Added recipientRegNumber/recipientRegDate fields for outgoing docs (deadline triggers from recipient registration date) - Added prelungire-CU deadline type in catalog (15 calendar days, tacit approval) - CU category already first in catalog verified - DeadlineAuditEntry interface + audit log on TrackedDeadline (created/resolved entries) - Document expiry tracking: expiryDate + expiryAlertDays with live countdown - Web scraping prep fields: externalStatusUrl + externalTrackingId - Dashboard: 6 stat cards (added missing recipient + expiring soon) - Alert banners for missing recipient data and expiring documents - Version bump to 0.2.0
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
"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';
|
||||
import { useState } from "react";
|
||||
import { Clock, CheckCircle2, X, History } 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;
|
||||
@@ -15,78 +16,156 @@ interface DeadlineCardProps {
|
||||
}
|
||||
|
||||
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',
|
||||
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',
|
||||
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) {
|
||||
export function DeadlineCard({
|
||||
deadline,
|
||||
onResolve,
|
||||
onRemove,
|
||||
}: DeadlineCardProps) {
|
||||
const def = getDeadlineType(deadline.typeId);
|
||||
const status = getDeadlineDisplayStatus(deadline);
|
||||
const [showAudit, setShowAudit] = useState(false);
|
||||
const auditLog = deadline.auditLog ?? [];
|
||||
|
||||
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>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border p-3",
|
||||
VARIANT_CLASSES[status.variant] ?? "",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<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>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
</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' && (
|
||||
<div className="flex gap-1 shrink-0">
|
||||
{auditLog.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground"
|
||||
onClick={() => setShowAudit(!showAudit)}
|
||||
title="Istoric modificări"
|
||||
>
|
||||
<History className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{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-green-600"
|
||||
onClick={() => onResolve(deadline)}
|
||||
title="Rezolvă"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={() => onRemove(deadline.id)}
|
||||
title="Șterge"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
<X 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>
|
||||
{/* Audit log */}
|
||||
{showAudit && auditLog.length > 0 && (
|
||||
<div className="mt-2 border-t pt-2 space-y-1">
|
||||
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Istoric modificări
|
||||
</p>
|
||||
{auditLog.map((entry, i) => (
|
||||
<div key={i} className="flex items-start gap-2 text-[11px]">
|
||||
<span className="text-muted-foreground whitespace-nowrap">
|
||||
{formatDateTime(entry.timestamp)}
|
||||
</span>
|
||||
{entry.actor && (
|
||||
<span className="font-medium">{entry.actor}</span>
|
||||
)}
|
||||
<span className="text-muted-foreground">{entry.detail}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
return new Date(iso).toLocaleDateString("ro-RO", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleString("ro-RO", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
|
||||
@@ -1,55 +1,83 @@
|
||||
'use client';
|
||||
"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';
|
||||
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;
|
||||
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',
|
||||
pending: "În așteptare",
|
||||
completed: "Finalizat",
|
||||
"aprobat-tacit": "Aprobat tacit",
|
||||
respins: "Respins",
|
||||
anulat: "Anulat",
|
||||
};
|
||||
|
||||
export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDeadline }: DeadlineDashboardProps) {
|
||||
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 [resolvingDeadline, setResolvingDeadline] =
|
||||
useState<TrackedDeadline | null>(null);
|
||||
|
||||
const stats = useMemo(() => aggregateDeadlines(entries), [entries]);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
return stats.all.filter((row) => {
|
||||
if (filters.category !== 'all') {
|
||||
if (filters.category !== "all") {
|
||||
const def = getDeadlineType(row.deadline.typeId);
|
||||
if (def && def.category !== filters.category) return false;
|
||||
}
|
||||
if (filters.resolution !== 'all') {
|
||||
if (filters.resolution !== "all") {
|
||||
// Map tacit display status to actual resolution filter
|
||||
if (filters.resolution === 'pending') {
|
||||
if (row.deadline.resolution !== 'pending') return false;
|
||||
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;
|
||||
if (row.status.variant !== "yellow" && row.status.variant !== "red")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -60,16 +88,31 @@ export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDead
|
||||
setResolvingDeadline(deadline);
|
||||
};
|
||||
|
||||
const handleResolve = (resolution: DeadlineResolution, note: string, chainNext: boolean) => {
|
||||
const handleResolve = (
|
||||
resolution: DeadlineResolution,
|
||||
note: string,
|
||||
chainNext: boolean,
|
||||
) => {
|
||||
if (!resolvingEntry || !resolvingDeadline) return;
|
||||
onResolveDeadline(resolvingEntry, resolvingDeadline.id, resolution, note, chainNext);
|
||||
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);
|
||||
onAddChainedDeadline(
|
||||
resolvingEntry,
|
||||
def.chainNextTypeId,
|
||||
resolvedDate,
|
||||
resolvingDeadline.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,43 +123,104 @@ export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDead
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<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} />
|
||||
<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}
|
||||
/>
|
||||
<StatCard
|
||||
label="Lipsă nr. dest."
|
||||
value={stats.missingRecipientReg}
|
||||
variant={stats.missingRecipientReg > 0 ? "destructive" : undefined}
|
||||
/>
|
||||
<StatCard
|
||||
label="Expiră curând"
|
||||
value={stats.expiringSoon}
|
||||
variant={stats.expiringSoon > 0 ? "destructive" : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Alert banners */}
|
||||
{stats.missingRecipientReg > 0 && (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-2 text-xs text-amber-700 dark:text-amber-400">
|
||||
⚠ {stats.missingRecipientReg} ieșir
|
||||
{stats.missingRecipientReg === 1 ? "e" : "i"} cu termene legale nu{" "}
|
||||
{stats.missingRecipientReg === 1 ? "are" : "au"} completat
|
||||
numărul/data de înregistrare la destinatar. Termenele legale nu
|
||||
pornesc fără aceste date.
|
||||
</div>
|
||||
)}
|
||||
{stats.expiringSoon > 0 && (
|
||||
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-4 py-2 text-xs text-red-700 dark:text-red-400">
|
||||
⚠ {stats.expiringSoon} document{stats.expiringSoon === 1 ? "" : "e"}{" "}
|
||||
cu termen de valabilitate se apropie de expirare sau{" "}
|
||||
{stats.expiringSoon === 1 ? "a" : "au"} expirat. Inițiați procedurile
|
||||
de prelungire.
|
||||
</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>
|
||||
<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>
|
||||
{(
|
||||
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>
|
||||
<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>
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant={filters.urgentOnly ? 'default' : 'outline'}
|
||||
variant={filters.urgentOnly ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => updateFilter('urgentOnly', !filters.urgentOnly)}
|
||||
onClick={() => updateFilter("urgentOnly", !filters.urgentOnly)}
|
||||
>
|
||||
Doar urgente
|
||||
</Button>
|
||||
@@ -144,14 +248,24 @@ export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDead
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' | 'blue' }) {
|
||||
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' : ''}`}>
|
||||
<p
|
||||
className={`text-2xl font-bold ${
|
||||
variant === "destructive" && value > 0 ? "text-destructive" : ""
|
||||
}${variant === "blue" && value > 0 ? "text-blue-600" : ""}`}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
@@ -228,6 +228,9 @@ export function RegistraturaModule() {
|
||||
[allEntries],
|
||||
);
|
||||
const urgentDeadlines = deadlineStats.urgent + deadlineStats.overdue;
|
||||
const missingRecipient = deadlineStats.missingRecipientReg;
|
||||
const expiringSoon = deadlineStats.expiringSoon;
|
||||
const alertCount = urgentDeadlines + missingRecipient + expiringSoon;
|
||||
|
||||
const closingEntry = closingId
|
||||
? allEntries.find((e) => e.id === closingId)
|
||||
@@ -248,12 +251,12 @@ export function RegistraturaModule() {
|
||||
<TabsTrigger value="registru">Registru</TabsTrigger>
|
||||
<TabsTrigger value="termene">
|
||||
Termene legale
|
||||
{urgentDeadlines > 0 && (
|
||||
{alertCount > 0 && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="ml-1.5 text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{urgentDeadlines}
|
||||
{alertCount}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
@@ -261,6 +264,21 @@ export function RegistraturaModule() {
|
||||
|
||||
<TabsContent value="registru">
|
||||
<div className="space-y-6">
|
||||
{/* Alert banners */}
|
||||
{missingRecipient > 0 && viewMode === "list" && (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-2 text-xs text-amber-700 dark:text-amber-400">
|
||||
⚠ {missingRecipient} ieșir{missingRecipient === 1 ? "e" : "i"} cu
|
||||
termene legale nu {missingRecipient === 1 ? "are" : "au"}{" "}
|
||||
completat datele de la destinatar.
|
||||
</div>
|
||||
)}
|
||||
{expiringSoon > 0 && viewMode === "list" && (
|
||||
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-4 py-2 text-xs text-red-700 dark:text-red-400">
|
||||
⚠ {expiringSoon} document{expiringSoon === 1 ? "" : "e"} se
|
||||
apropie de expirare sau {expiringSoon === 1 ? "a" : "au"} expirat.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Total" value={total} />
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
Info,
|
||||
GitBranch,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
Calendar,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
import type {
|
||||
@@ -149,6 +152,24 @@ export function RegistryEntryForm({
|
||||
const [linkedSearch, setLinkedSearch] = useState("");
|
||||
const [threadSearch, setThreadSearch] = useState("");
|
||||
|
||||
// ── 3.03 new fields ──
|
||||
const [recipientRegNumber, setRecipientRegNumber] = useState(
|
||||
initial?.recipientRegNumber ?? "",
|
||||
);
|
||||
const [recipientRegDate, setRecipientRegDate] = useState(
|
||||
initial?.recipientRegDate ?? "",
|
||||
);
|
||||
const [expiryDate, setExpiryDate] = useState(initial?.expiryDate ?? "");
|
||||
const [expiryAlertDays, setExpiryAlertDays] = useState(
|
||||
initial?.expiryAlertDays ?? 30,
|
||||
);
|
||||
const [externalStatusUrl, setExternalStatusUrl] = useState(
|
||||
initial?.externalStatusUrl ?? "",
|
||||
);
|
||||
const [externalTrackingId, setExternalTrackingId] = useState(
|
||||
initial?.externalTrackingId ?? "",
|
||||
);
|
||||
|
||||
// ── Submission lock + file upload tracking ──
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [uploadingCount, setUploadingCount] = useState(0);
|
||||
@@ -340,6 +361,12 @@ export function RegistryEntryForm({
|
||||
assignee: assignee || undefined,
|
||||
assigneeContactId: assigneeContactId || undefined,
|
||||
threadParentId: threadParentId || undefined,
|
||||
recipientRegNumber: recipientRegNumber || undefined,
|
||||
recipientRegDate: recipientRegDate || undefined,
|
||||
expiryDate: expiryDate || undefined,
|
||||
expiryAlertDays: expiryDate ? expiryAlertDays : undefined,
|
||||
externalStatusUrl: externalStatusUrl || undefined,
|
||||
externalTrackingId: externalTrackingId || undefined,
|
||||
linkedEntryIds,
|
||||
attachments,
|
||||
trackedDeadlines:
|
||||
@@ -595,7 +622,59 @@ export function RegistryEntryForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assignee (Responsabil) */}
|
||||
{/* Recipient registration fields — only for outgoing (iesit) documents */}
|
||||
{direction === "iesit" && (
|
||||
<div className="rounded-md border border-blue-500/30 bg-blue-500/5 p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<Label className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||
Înregistrare la destinatar
|
||||
</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Termenul legal pentru ieșiri curge DOAR de la data
|
||||
înregistrării la destinatar, nu de la data trimiterii.
|
||||
Completează aceste câmpuri când primești confirmarea.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label className="text-xs">Nr. înregistrare destinatar</Label>
|
||||
<Input
|
||||
value={recipientRegNumber}
|
||||
onChange={(e) => setRecipientRegNumber(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Ex: 12345/2026"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">Data înregistrare destinatar</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={recipientRegDate}
|
||||
onChange={(e) => setRecipientRegDate(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!recipientRegNumber && !recipientRegDate && (
|
||||
<p className="text-[10px] text-amber-600 dark:text-amber-400">
|
||||
Atenție: Datele de la destinatar nu sunt completate. Termenele
|
||||
legale atașate nu vor porni până la completarea lor.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assignee (Responsabil) — kept below */}
|
||||
<div className="relative">
|
||||
<Label className="flex items-center gap-1.5">
|
||||
Responsabil
|
||||
@@ -707,6 +786,104 @@ export function RegistryEntryForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document Expiry — CU/AC validity tracking */}
|
||||
<div className="rounded-md border border-muted p-3 space-y-3">
|
||||
<Label className="flex items-center gap-1.5 text-sm font-medium">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Valabilitate document
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Pentru documente cu termen de valabilitate (CU, AC etc.).
|
||||
Sistemul va genera alerte înainte de expirare.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label className="text-xs">Data expirare</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={expiryDate}
|
||||
onChange={(e) => setExpiryDate(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
{expiryDate && (
|
||||
<div>
|
||||
<Label className="text-xs">Alertă cu X zile înainte</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={expiryAlertDays}
|
||||
onChange={(e) =>
|
||||
setExpiryAlertDays(parseInt(e.target.value, 10) || 30)
|
||||
}
|
||||
className="mt-1"
|
||||
min={1}
|
||||
max={365}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{expiryDate &&
|
||||
(() => {
|
||||
const expiry = new Date(expiryDate);
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
expiry.setHours(0, 0, 0, 0);
|
||||
const daysLeft = Math.ceil(
|
||||
(expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
if (daysLeft < 0) {
|
||||
return (
|
||||
<p className="text-[10px] text-red-600 dark:text-red-400 font-medium">
|
||||
Document expirat de {Math.abs(daysLeft)} zile!
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (daysLeft <= expiryAlertDays) {
|
||||
return (
|
||||
<p className="text-[10px] text-amber-600 dark:text-amber-400">
|
||||
Expiră în {daysLeft} zile — inițiați procedurile de
|
||||
prelungire.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Web scraping prep — external tracking */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label className="flex items-center gap-1.5 text-xs">
|
||||
<Globe className="h-3 w-3" />
|
||||
URL verificare status (opțional)
|
||||
</Label>
|
||||
<Input
|
||||
value={externalStatusUrl}
|
||||
onChange={(e) => setExternalStatusUrl(e.target.value)}
|
||||
className="mt-1 text-xs"
|
||||
placeholder="https://portal.primaria.ro/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">ID urmărire extern (opțional)</Label>
|
||||
<Input
|
||||
value={externalTrackingId}
|
||||
onChange={(e) => setExternalTrackingId(e.target.value)}
|
||||
className="mt-1 text-xs"
|
||||
placeholder="Ex: REF-2026-001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thread parent — reply to another entry */}
|
||||
{allEntries && allEntries.length > 0 && (
|
||||
<div>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { ModuleConfig } from '@/core/module-registry/types';
|
||||
import type { ModuleConfig } from "@/core/module-registry/types";
|
||||
|
||||
export const registraturaConfig: ModuleConfig = {
|
||||
id: 'registratura',
|
||||
name: 'Registratură',
|
||||
description: 'Registru de corespondență multi-firmă cu urmărire documente',
|
||||
icon: 'book-open',
|
||||
route: '/registratura',
|
||||
category: 'operations',
|
||||
featureFlag: 'module.registratura',
|
||||
visibility: 'all',
|
||||
version: '0.1.0',
|
||||
id: "registratura",
|
||||
name: "Registratură",
|
||||
description:
|
||||
"Registru de corespondență cu termene legale, audit log și urmărire valabilitate",
|
||||
icon: "book-open",
|
||||
route: "/registratura",
|
||||
category: "operations",
|
||||
featureFlag: "module.registratura",
|
||||
visibility: "all",
|
||||
version: "0.2.0",
|
||||
dependencies: [],
|
||||
storageNamespace: 'registratura',
|
||||
storageNamespace: "registratura",
|
||||
navOrder: 10,
|
||||
tags: ['registru', 'corespondență', 'documente'],
|
||||
tags: ["registru", "corespondență", "documente"],
|
||||
};
|
||||
|
||||
@@ -11,5 +11,6 @@ export type {
|
||||
DeadlineCategory,
|
||||
DeadlineTypeDef,
|
||||
TrackedDeadline,
|
||||
DeadlineAuditEntry,
|
||||
} from "./types";
|
||||
export { DEFAULT_DOCUMENT_TYPES, DEFAULT_DOC_TYPE_LABELS } from "./types";
|
||||
|
||||
@@ -15,6 +15,20 @@ export const DEADLINE_CATALOG: DeadlineTypeDef[] = [
|
||||
category: "certificat",
|
||||
legalReference: "Legea 50/1991, art. 6¹",
|
||||
},
|
||||
{
|
||||
id: "prelungire-cu",
|
||||
label: "Cerere prelungire CU",
|
||||
description:
|
||||
"Cerere de prelungire a Certificatului de Urbanism. Se depune înainte de expirare.",
|
||||
days: 15,
|
||||
dayType: "calendar",
|
||||
startDateLabel: "Data depunerii cererii de prelungire",
|
||||
requiresCustomStartDate: true,
|
||||
startDateHint: "Data la care s-a depus cererea de prelungire a CU",
|
||||
tacitApprovalApplicable: true,
|
||||
category: "certificat",
|
||||
legalReference: "Legea 50/1991, art. 6¹",
|
||||
},
|
||||
|
||||
// ── Avize ──
|
||||
{
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { TrackedDeadline, DeadlineResolution, RegistryEntry } from '../types';
|
||||
import { getDeadlineType } from './deadline-catalog';
|
||||
import { computeDueDate } from './working-days';
|
||||
import { v4 as uuid } from "uuid";
|
||||
import type {
|
||||
TrackedDeadline,
|
||||
DeadlineResolution,
|
||||
RegistryEntry,
|
||||
DeadlineAuditEntry,
|
||||
} from "../types";
|
||||
import { getDeadlineType } from "./deadline-catalog";
|
||||
import { computeDueDate } from "./working-days";
|
||||
|
||||
export interface DeadlineDisplayStatus {
|
||||
label: string;
|
||||
variant: 'green' | 'yellow' | 'red' | 'blue' | 'gray';
|
||||
variant: "green" | "yellow" | "red" | "blue" | "gray";
|
||||
daysRemaining: number | null;
|
||||
}
|
||||
|
||||
@@ -23,15 +28,27 @@ export function createTrackedDeadline(
|
||||
const start = new Date(startDate);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
|
||||
const due = computeDueDate(start, def.days, def.dayType, def.isBackwardDeadline);
|
||||
const due = computeDueDate(
|
||||
start,
|
||||
def.days,
|
||||
def.dayType,
|
||||
def.isBackwardDeadline,
|
||||
);
|
||||
|
||||
return {
|
||||
id: uuid(),
|
||||
typeId,
|
||||
startDate,
|
||||
dueDate: formatDate(due),
|
||||
resolution: 'pending',
|
||||
resolution: "pending",
|
||||
chainParentId,
|
||||
auditLog: [
|
||||
{
|
||||
action: "created",
|
||||
timestamp: new Date().toISOString(),
|
||||
detail: `Termen creat: ${def.label} (${def.days} ${def.dayType === "working" ? "zile lucrătoare" : "zile calendaristice"})`,
|
||||
},
|
||||
],
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -44,32 +61,40 @@ export function resolveDeadline(
|
||||
resolution: DeadlineResolution,
|
||||
note?: string,
|
||||
): TrackedDeadline {
|
||||
const auditEntry: DeadlineAuditEntry = {
|
||||
action: "resolved",
|
||||
timestamp: new Date().toISOString(),
|
||||
detail: `Rezolvat: ${resolution}${note ? ` — ${note}` : ""}`,
|
||||
};
|
||||
return {
|
||||
...deadline,
|
||||
resolution,
|
||||
resolvedDate: new Date().toISOString(),
|
||||
resolutionNote: note,
|
||||
auditLog: [...(deadline.auditLog ?? []), auditEntry],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display status for a tracked deadline — color coding + label.
|
||||
*/
|
||||
export function getDeadlineDisplayStatus(deadline: TrackedDeadline): DeadlineDisplayStatus {
|
||||
export function getDeadlineDisplayStatus(
|
||||
deadline: TrackedDeadline,
|
||||
): DeadlineDisplayStatus {
|
||||
const def = getDeadlineType(deadline.typeId);
|
||||
|
||||
// Already resolved
|
||||
if (deadline.resolution !== 'pending') {
|
||||
if (deadline.resolution === 'aprobat-tacit') {
|
||||
return { label: 'Aprobat tacit', variant: 'blue', daysRemaining: null };
|
||||
if (deadline.resolution !== "pending") {
|
||||
if (deadline.resolution === "aprobat-tacit") {
|
||||
return { label: "Aprobat tacit", variant: "blue", daysRemaining: null };
|
||||
}
|
||||
if (deadline.resolution === 'respins') {
|
||||
return { label: 'Respins', variant: 'gray', daysRemaining: null };
|
||||
if (deadline.resolution === "respins") {
|
||||
return { label: "Respins", variant: "gray", daysRemaining: null };
|
||||
}
|
||||
if (deadline.resolution === 'anulat') {
|
||||
return { label: 'Anulat', variant: 'gray', daysRemaining: null };
|
||||
if (deadline.resolution === "anulat") {
|
||||
return { label: "Anulat", variant: "gray", daysRemaining: null };
|
||||
}
|
||||
return { label: 'Finalizat', variant: 'gray', daysRemaining: null };
|
||||
return { label: "Finalizat", variant: "gray", daysRemaining: null };
|
||||
}
|
||||
|
||||
// Pending — compute days remaining
|
||||
@@ -83,16 +108,16 @@ export function getDeadlineDisplayStatus(deadline: TrackedDeadline): DeadlineDis
|
||||
|
||||
// Overdue + tacit applicable → tacit approval
|
||||
if (daysRemaining < 0 && def?.tacitApprovalApplicable) {
|
||||
return { label: 'Aprobat tacit', variant: 'blue', daysRemaining };
|
||||
return { label: "Aprobat tacit", variant: "blue", daysRemaining };
|
||||
}
|
||||
|
||||
if (daysRemaining < 0) {
|
||||
return { label: 'Depășit termen', variant: 'red', daysRemaining };
|
||||
return { label: "Depășit termen", variant: "red", daysRemaining };
|
||||
}
|
||||
if (daysRemaining <= 5) {
|
||||
return { label: 'Urgent', variant: 'yellow', daysRemaining };
|
||||
return { label: "Urgent", variant: "yellow", daysRemaining };
|
||||
}
|
||||
return { label: 'În termen', variant: 'green', daysRemaining };
|
||||
return { label: "În termen", variant: "green", daysRemaining };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,25 +128,64 @@ export function aggregateDeadlines(entries: RegistryEntry[]): {
|
||||
urgent: number;
|
||||
overdue: number;
|
||||
tacit: number;
|
||||
all: Array<{ deadline: TrackedDeadline; entry: RegistryEntry; status: DeadlineDisplayStatus }>;
|
||||
missingRecipientReg: number;
|
||||
expiringSoon: number;
|
||||
all: Array<{
|
||||
deadline: TrackedDeadline;
|
||||
entry: RegistryEntry;
|
||||
status: DeadlineDisplayStatus;
|
||||
}>;
|
||||
} {
|
||||
let active = 0;
|
||||
let urgent = 0;
|
||||
let overdue = 0;
|
||||
let tacit = 0;
|
||||
const all: Array<{ deadline: TrackedDeadline; entry: RegistryEntry; status: DeadlineDisplayStatus }> = [];
|
||||
const all: Array<{
|
||||
deadline: TrackedDeadline;
|
||||
entry: RegistryEntry;
|
||||
status: DeadlineDisplayStatus;
|
||||
}> = [];
|
||||
|
||||
// Count entries missing recipient registration (outgoing with deadlines)
|
||||
let missingRecipientReg = 0;
|
||||
let expiringSoon = 0;
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
for (const entry of entries) {
|
||||
// Check missing recipient registration for outgoing entries
|
||||
if (
|
||||
entry.direction === "iesit" &&
|
||||
entry.status === "deschis" &&
|
||||
(entry.trackedDeadlines ?? []).length > 0 &&
|
||||
!entry.recipientRegDate
|
||||
) {
|
||||
missingRecipientReg++;
|
||||
}
|
||||
|
||||
// Check document expiry
|
||||
if (entry.expiryDate && entry.status === "deschis") {
|
||||
const expiry = new Date(entry.expiryDate);
|
||||
expiry.setHours(0, 0, 0, 0);
|
||||
const daysLeft = Math.ceil(
|
||||
(expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
const alertDays = entry.expiryAlertDays ?? 30;
|
||||
if (daysLeft <= alertDays) {
|
||||
expiringSoon++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const dl of entry.trackedDeadlines ?? []) {
|
||||
const status = getDeadlineDisplayStatus(dl);
|
||||
all.push({ deadline: dl, entry, status });
|
||||
|
||||
if (dl.resolution === 'pending') {
|
||||
if (dl.resolution === "pending") {
|
||||
active++;
|
||||
if (status.variant === 'yellow') urgent++;
|
||||
if (status.variant === 'red') overdue++;
|
||||
if (status.variant === 'blue') tacit++;
|
||||
} else if (dl.resolution === 'aprobat-tacit') {
|
||||
if (status.variant === "yellow") urgent++;
|
||||
if (status.variant === "red") overdue++;
|
||||
if (status.variant === "blue") tacit++;
|
||||
} else if (dl.resolution === "aprobat-tacit") {
|
||||
tacit++;
|
||||
}
|
||||
}
|
||||
@@ -129,18 +193,26 @@ export function aggregateDeadlines(entries: RegistryEntry[]): {
|
||||
|
||||
// Sort: overdue first, then by due date ascending
|
||||
all.sort((a, b) => {
|
||||
const aP = a.deadline.resolution === 'pending' ? 0 : 1;
|
||||
const bP = b.deadline.resolution === 'pending' ? 0 : 1;
|
||||
const aP = a.deadline.resolution === "pending" ? 0 : 1;
|
||||
const bP = b.deadline.resolution === "pending" ? 0 : 1;
|
||||
if (aP !== bP) return aP - bP;
|
||||
return a.deadline.dueDate.localeCompare(b.deadline.dueDate);
|
||||
});
|
||||
|
||||
return { active, urgent, overdue, tacit, all };
|
||||
return {
|
||||
active,
|
||||
urgent,
|
||||
overdue,
|
||||
tacit,
|
||||
missingRecipientReg,
|
||||
expiringSoon,
|
||||
all,
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,15 @@ export interface DeadlineTypeDef {
|
||||
isBackwardDeadline?: boolean;
|
||||
}
|
||||
|
||||
/** Audit log entry for deadline changes */
|
||||
export interface DeadlineAuditEntry {
|
||||
action: "created" | "resolved" | "modified" | "recipient-registered";
|
||||
timestamp: string;
|
||||
/** User who performed the action (SSO name when available) */
|
||||
actor?: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface TrackedDeadline {
|
||||
id: string;
|
||||
typeId: string;
|
||||
@@ -125,6 +134,8 @@ export interface TrackedDeadline {
|
||||
resolvedDate?: string;
|
||||
resolutionNote?: string;
|
||||
chainParentId?: string;
|
||||
/** Mini audit log — tracks who created/modified/resolved this deadline */
|
||||
auditLog?: DeadlineAuditEntry[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -145,6 +156,10 @@ export interface RegistryEntry {
|
||||
/** Destinatar — free text or linked contact ID */
|
||||
recipient: string;
|
||||
recipientContactId?: string;
|
||||
/** Număr înregistrare la destinatar (for outgoing docs — triggers legal deadline) */
|
||||
recipientRegNumber?: string;
|
||||
/** Data înregistrare la destinatar (YYYY-MM-DD) — legal deadlines start from this date */
|
||||
recipientRegDate?: string;
|
||||
company: CompanyId;
|
||||
status: RegistryStatus;
|
||||
/** Structured closure metadata (populated when status = 'inchis') */
|
||||
@@ -162,6 +177,14 @@ export interface RegistryEntry {
|
||||
attachments: RegistryAttachment[];
|
||||
/** Tracked legal deadlines */
|
||||
trackedDeadlines?: TrackedDeadline[];
|
||||
/** Document expiry date — for CU/AC validity tracking (YYYY-MM-DD) */
|
||||
expiryDate?: string;
|
||||
/** Days before expiry to trigger alert (default 30) */
|
||||
expiryAlertDays?: number;
|
||||
/** URL for external status checking (web scraping prep) */
|
||||
externalStatusUrl?: string;
|
||||
/** External tracking ID (e.g., portal reference number) */
|
||||
externalTrackingId?: string;
|
||||
tags: string[];
|
||||
notes: string;
|
||||
visibility: Visibility;
|
||||
|
||||
Reference in New Issue
Block a user