feat: timeline milestones for deadlines, auto-close reply entries, cleanup
- Entry created via "Inchide" flow now gets status "inchis" with closureInfo - New DeadlineTimeline component: main deadlines as cards with progress bar, auto-tracked sub-deadlines as milestone dots on horizontal timeline - Auto-tracked deadlines hidden from dashboard when user deadlines exist - Verification milestone shows "Expirat — nu se mai pot solicita clarificari" - Parent closureInfo now includes linkedEntryId/Number of the closing act - Removed orphaned deadline-table.tsx and use-deadline-filters.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,127 +0,0 @@
|
||||
'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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import type { TrackedDeadline } from "../types";
|
||||
import { getDeadlineType } from "../services/deadline-catalog";
|
||||
import { getDeadlineDisplayStatus } from "../services/deadline-service";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
// ── Types ──
|
||||
|
||||
interface TimelineMilestone {
|
||||
deadline: TrackedDeadline;
|
||||
label: string;
|
||||
description: string;
|
||||
dueDate: string;
|
||||
/** Position on the timeline (0..1) */
|
||||
position: number;
|
||||
/** Status of this milestone */
|
||||
status: "completed" | "active" | "expired" | "upcoming" | "background";
|
||||
/** Human-readable status text */
|
||||
statusText: string;
|
||||
/** Whether this is the main (user-created) deadline or an auto-tracked milestone */
|
||||
isMain: boolean;
|
||||
/** Days remaining (negative = overdue) */
|
||||
daysRemaining: number;
|
||||
}
|
||||
|
||||
interface DeadlineTimelineProps {
|
||||
/** All tracked deadlines on this entry */
|
||||
deadlines: TrackedDeadline[];
|
||||
}
|
||||
|
||||
// ── Build timeline from deadlines ──
|
||||
|
||||
function buildTimeline(deadlines: TrackedDeadline[]): {
|
||||
mainDeadlines: TimelineMilestone[];
|
||||
groups: Map<string, TimelineMilestone[]>;
|
||||
} {
|
||||
const mainDeadlines: TimelineMilestone[] = [];
|
||||
// Group auto-tracked by their chain parent (or by category proximity to a main deadline)
|
||||
const groups = new Map<string, TimelineMilestone[]>();
|
||||
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
// Separate main vs auto-tracked deadlines
|
||||
const mainDls: TrackedDeadline[] = [];
|
||||
const autoDls: TrackedDeadline[] = [];
|
||||
|
||||
for (const dl of deadlines) {
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
if (def?.backgroundOnly) continue; // Skip background-only entirely
|
||||
if (def?.autoTrack) {
|
||||
autoDls.push(dl);
|
||||
} else {
|
||||
mainDls.push(dl);
|
||||
}
|
||||
}
|
||||
|
||||
// For each main deadline, find its associated milestones
|
||||
for (const main of mainDls) {
|
||||
const def = getDeadlineType(main.typeId);
|
||||
const mainStart = new Date(main.startDate);
|
||||
mainStart.setHours(0, 0, 0, 0);
|
||||
const mainDue = new Date(main.dueDate);
|
||||
mainDue.setHours(0, 0, 0, 0);
|
||||
const totalSpan = Math.max(1, mainDue.getTime() - mainStart.getTime());
|
||||
const mainDaysRemaining = Math.ceil(
|
||||
(mainDue.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
const mainStatus = getMainStatus(main, mainDaysRemaining);
|
||||
|
||||
mainDeadlines.push({
|
||||
deadline: main,
|
||||
label: def?.label ?? main.typeId,
|
||||
description: def?.description ?? "",
|
||||
dueDate: main.dueDate,
|
||||
position: 1,
|
||||
status: mainStatus,
|
||||
statusText: getStatusText(mainStatus, mainDaysRemaining),
|
||||
isMain: true,
|
||||
daysRemaining: mainDaysRemaining,
|
||||
});
|
||||
|
||||
// Find auto-tracked milestones that fall within this main deadline's range
|
||||
const milestones: TimelineMilestone[] = [];
|
||||
for (const auto of autoDls) {
|
||||
const autoDef = getDeadlineType(auto.typeId);
|
||||
if (!autoDef) continue;
|
||||
|
||||
// Check if this auto-tracked deadline is related (same start date or chain parent in this group)
|
||||
const autoStart = new Date(auto.startDate);
|
||||
autoStart.setHours(0, 0, 0, 0);
|
||||
const autoDue = new Date(auto.dueDate);
|
||||
autoDue.setHours(0, 0, 0, 0);
|
||||
|
||||
// Only include if it starts from the same point or is within the main's time range
|
||||
const startsNearMain =
|
||||
Math.abs(autoStart.getTime() - mainStart.getTime()) <
|
||||
2 * 24 * 60 * 60 * 1000;
|
||||
const isChainRelated = auto.chainParentId
|
||||
? deadlines.some(
|
||||
(d) =>
|
||||
d.id === auto.chainParentId &&
|
||||
(d.typeId === main.typeId ||
|
||||
d.chainParentId === main.id),
|
||||
)
|
||||
: false;
|
||||
|
||||
if (!startsNearMain && !isChainRelated) continue;
|
||||
|
||||
const pos = Math.min(
|
||||
0.95,
|
||||
Math.max(
|
||||
0.05,
|
||||
(autoDue.getTime() - mainStart.getTime()) / totalSpan,
|
||||
),
|
||||
);
|
||||
const autoDaysRemaining = Math.ceil(
|
||||
(autoDue.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
const autoStatus = getAutoStatus(auto, autoDaysRemaining, autoDef.id);
|
||||
|
||||
milestones.push({
|
||||
deadline: auto,
|
||||
label: autoDef.label,
|
||||
description: getExpiredDescription(autoDef.id, autoStatus),
|
||||
dueDate: auto.dueDate,
|
||||
position: pos,
|
||||
status: autoStatus,
|
||||
statusText: getAutoStatusText(autoDef.id, autoStatus, autoDaysRemaining),
|
||||
isMain: false,
|
||||
daysRemaining: autoDaysRemaining,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort milestones by position
|
||||
milestones.sort((a, b) => a.position - b.position);
|
||||
if (milestones.length > 0) {
|
||||
groups.set(main.id, milestones);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle auto-tracked deadlines that aren't associated with any main deadline
|
||||
const associatedAutoIds = new Set<string>();
|
||||
for (const ms of groups.values()) {
|
||||
for (const m of ms) {
|
||||
associatedAutoIds.add(m.deadline.id);
|
||||
}
|
||||
}
|
||||
const orphanedAuto = autoDls.filter(
|
||||
(dl) => !associatedAutoIds.has(dl.id),
|
||||
);
|
||||
|
||||
if (orphanedAuto.length > 0 && mainDls.length === 0) {
|
||||
// If there are ONLY auto-tracked deadlines (no main), show them as standalone milestones
|
||||
for (const auto of orphanedAuto) {
|
||||
const autoDef = getDeadlineType(auto.typeId);
|
||||
if (!autoDef) continue;
|
||||
const autoDue = new Date(auto.dueDate);
|
||||
autoDue.setHours(0, 0, 0, 0);
|
||||
const daysRemaining = Math.ceil(
|
||||
(autoDue.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
const status = getMainStatus(auto, daysRemaining);
|
||||
|
||||
mainDeadlines.push({
|
||||
deadline: auto,
|
||||
label: autoDef.label,
|
||||
description: autoDef.description,
|
||||
dueDate: auto.dueDate,
|
||||
position: 1,
|
||||
status,
|
||||
statusText: getStatusText(status, daysRemaining),
|
||||
isMain: true,
|
||||
daysRemaining,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { mainDeadlines, groups };
|
||||
}
|
||||
|
||||
function getMainStatus(
|
||||
dl: TrackedDeadline,
|
||||
daysRemaining: number,
|
||||
): TimelineMilestone["status"] {
|
||||
if (dl.resolution === "completed") return "completed";
|
||||
if (dl.resolution !== "pending") return "completed";
|
||||
if (daysRemaining < 0) return "expired";
|
||||
if (daysRemaining <= 5) return "active";
|
||||
return "upcoming";
|
||||
}
|
||||
|
||||
function getAutoStatus(
|
||||
dl: TrackedDeadline,
|
||||
daysRemaining: number,
|
||||
typeId: string,
|
||||
): TimelineMilestone["status"] {
|
||||
if (dl.resolution !== "pending") return "completed";
|
||||
if (daysRemaining < 0) {
|
||||
// For verification-type deadlines, "expired" means the window has passed
|
||||
if (typeId.includes("verificare")) return "expired";
|
||||
return "expired";
|
||||
}
|
||||
if (daysRemaining <= 2) return "active";
|
||||
return "upcoming";
|
||||
}
|
||||
|
||||
function getStatusText(
|
||||
status: TimelineMilestone["status"],
|
||||
daysRemaining: number,
|
||||
): string {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "Finalizat";
|
||||
case "expired":
|
||||
return daysRemaining < 0
|
||||
? `${Math.abs(daysRemaining)}z depasit`
|
||||
: "Depasit";
|
||||
case "active":
|
||||
return daysRemaining === 0
|
||||
? "Astazi"
|
||||
: `${daysRemaining}z ramase`;
|
||||
case "upcoming":
|
||||
return `${daysRemaining}z ramase`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getAutoStatusText(
|
||||
typeId: string,
|
||||
status: TimelineMilestone["status"],
|
||||
daysRemaining: number,
|
||||
): string {
|
||||
if (status === "completed") return "Finalizat";
|
||||
if (status === "expired") {
|
||||
if (typeId.includes("verificare")) {
|
||||
return "Expirat — nu se mai pot solicita clarificari";
|
||||
}
|
||||
return `Depasit cu ${Math.abs(daysRemaining)}z`;
|
||||
}
|
||||
if (daysRemaining === 0) return "Astazi";
|
||||
return `${daysRemaining}z`;
|
||||
}
|
||||
|
||||
function getExpiredDescription(
|
||||
typeId: string,
|
||||
status: TimelineMilestone["status"],
|
||||
): string {
|
||||
if (status === "expired" && typeId.includes("verificare")) {
|
||||
return "Posibilitatea de a solicita clarificari sau de a returna documentatia a expirat.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatShortDate(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Component ──
|
||||
|
||||
export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) {
|
||||
const { mainDeadlines, groups } = useMemo(
|
||||
() => buildTimeline(deadlines),
|
||||
[deadlines],
|
||||
);
|
||||
|
||||
if (mainDeadlines.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{mainDeadlines.map((main) => {
|
||||
const milestones = groups.get(main.deadline.id) ?? [];
|
||||
const displayStatus = getDeadlineDisplayStatus(main.deadline);
|
||||
|
||||
return (
|
||||
<div key={main.deadline.id} className="space-y-2">
|
||||
{/* Main deadline header */}
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2",
|
||||
main.status === "expired"
|
||||
? "border-destructive/40 bg-destructive/5"
|
||||
: main.status === "active"
|
||||
? "border-amber-300 bg-amber-50/30 dark:border-amber-800 dark:bg-amber-950/20"
|
||||
: main.status === "completed"
|
||||
? "border-muted bg-muted/20"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">{main.label}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
displayStatus.variant === "red" &&
|
||||
"border-red-400 text-red-700 dark:text-red-400",
|
||||
displayStatus.variant === "yellow" &&
|
||||
"border-amber-400 text-amber-700 dark:text-amber-400",
|
||||
displayStatus.variant === "green" &&
|
||||
"border-green-400 text-green-700 dark:text-green-400",
|
||||
displayStatus.variant === "blue" &&
|
||||
"border-blue-400 text-blue-700 dark:text-blue-400",
|
||||
displayStatus.variant === "gray" &&
|
||||
"border-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{displayStatus.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center gap-3 text-[10px] text-muted-foreground">
|
||||
<span>Start: {formatShortDate(main.deadline.startDate)}</span>
|
||||
<span>Scadent: {formatShortDate(main.dueDate)}</span>
|
||||
{main.daysRemaining > 0 && main.status !== "completed" && (
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
main.daysRemaining <= 5
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-foreground",
|
||||
)}
|
||||
>
|
||||
{main.daysRemaining}z ramase
|
||||
</span>
|
||||
)}
|
||||
{main.daysRemaining < 0 && main.status === "expired" && (
|
||||
<span className="font-medium text-destructive">
|
||||
{Math.abs(main.daysRemaining)}z depasit
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{main.deadline.resolutionNote && (
|
||||
<p className="mt-1 text-[10px] text-muted-foreground italic">
|
||||
{main.deadline.resolutionNote}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Progress bar */}
|
||||
{main.status !== "completed" && (
|
||||
<ProgressBar deadline={main.deadline} status={main.status} />
|
||||
)}
|
||||
|
||||
{/* Milestones timeline */}
|
||||
{milestones.length > 0 && (
|
||||
<MilestoneTimeline
|
||||
milestones={milestones}
|
||||
mainDeadline={main.deadline}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Progress bar ──
|
||||
|
||||
function ProgressBar({
|
||||
deadline,
|
||||
status,
|
||||
}: {
|
||||
deadline: TrackedDeadline;
|
||||
status: TimelineMilestone["status"];
|
||||
}) {
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
const start = new Date(deadline.startDate);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const due = new Date(deadline.dueDate);
|
||||
due.setHours(0, 0, 0, 0);
|
||||
|
||||
const total = Math.max(1, due.getTime() - start.getTime());
|
||||
const elapsed = now.getTime() - start.getTime();
|
||||
const pct = Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
|
||||
return (
|
||||
<div className="mt-2 h-1.5 rounded-full bg-muted">
|
||||
<div
|
||||
className={cn(
|
||||
"h-1.5 rounded-full transition-all",
|
||||
status === "expired"
|
||||
? "bg-red-500"
|
||||
: status === "active"
|
||||
? "bg-amber-500"
|
||||
: "bg-green-500",
|
||||
)}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Milestone timeline (horizontal dots along the progress bar) ──
|
||||
|
||||
function MilestoneTimeline({
|
||||
milestones,
|
||||
mainDeadline,
|
||||
}: {
|
||||
milestones: TimelineMilestone[];
|
||||
mainDeadline: TrackedDeadline;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-3 space-y-0">
|
||||
{/* Timeline track */}
|
||||
<div className="relative h-6">
|
||||
{/* Base line */}
|
||||
<div className="absolute top-[10px] left-0 right-0 h-px bg-border" />
|
||||
|
||||
{/* Start marker */}
|
||||
<div className="absolute top-[7px] left-0 h-1.5 w-1.5 rounded-full bg-muted-foreground" />
|
||||
|
||||
{/* End marker (main deadline) */}
|
||||
<div className="absolute top-[7px] right-0 h-1.5 w-1.5 rounded-full bg-muted-foreground" />
|
||||
|
||||
{/* Milestone dots */}
|
||||
{milestones.map((ms) => (
|
||||
<div
|
||||
key={ms.deadline.id}
|
||||
className="absolute top-0"
|
||||
style={{ left: `${ms.position * 100}%` }}
|
||||
title={`${ms.label}: ${ms.statusText}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-5 w-5 -ml-2.5 rounded-full border-2 flex items-center justify-center",
|
||||
ms.status === "completed"
|
||||
? "border-green-500 bg-green-100 dark:bg-green-900/40"
|
||||
: ms.status === "expired"
|
||||
? "border-muted-foreground/40 bg-muted"
|
||||
: ms.status === "active"
|
||||
? "border-amber-500 bg-amber-100 dark:bg-amber-900/40"
|
||||
: "border-border bg-background",
|
||||
)}
|
||||
>
|
||||
{ms.status === "completed" && (
|
||||
<span className="text-[8px] text-green-700 dark:text-green-300">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
{ms.status === "expired" && (
|
||||
<span className="text-[8px] text-muted-foreground">—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Milestone labels below */}
|
||||
<div className="space-y-1.5 pt-1">
|
||||
{milestones.map((ms) => (
|
||||
<div key={ms.deadline.id} className="flex items-start gap-2 text-[10px]">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full",
|
||||
ms.status === "completed"
|
||||
? "bg-green-500"
|
||||
: ms.status === "expired"
|
||||
? "bg-muted-foreground/40"
|
||||
: ms.status === "active"
|
||||
? "bg-amber-500"
|
||||
: "bg-border",
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
ms.status === "expired" && "text-muted-foreground line-through",
|
||||
ms.status === "completed" && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{ms.label}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{formatShortDate(ms.dueDate)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
ms.status === "expired"
|
||||
? "text-muted-foreground italic"
|
||||
: ms.status === "active"
|
||||
? "text-amber-600 dark:text-amber-400 font-medium"
|
||||
: ms.status === "completed"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{ms.statusText}
|
||||
</span>
|
||||
</div>
|
||||
{ms.description && (
|
||||
<p className="text-muted-foreground italic">{ms.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -168,18 +168,37 @@ export function RegistraturaModule() {
|
||||
);
|
||||
}
|
||||
|
||||
const closureInfo: ClosureInfo = {
|
||||
const parentClosureInfo: ClosureInfo = {
|
||||
resolution: "finalizat",
|
||||
reason: "Inchis prin inregistrare conex",
|
||||
closedBy: "",
|
||||
closedAt: new Date().toISOString(),
|
||||
hadActiveDeadlines: resolvable.length > 0,
|
||||
linkedEntryId: newEntry?.id,
|
||||
linkedEntryNumber: newEntry?.number,
|
||||
};
|
||||
await updateEntry(closesEntryId, {
|
||||
closureInfo,
|
||||
closureInfo: parentClosureInfo,
|
||||
trackedDeadlines: updatedDeadlines,
|
||||
});
|
||||
await closeEntry(closesEntryId, false);
|
||||
|
||||
// Also close the new entry (act administrativ) — it was created to close the parent
|
||||
if (newEntry) {
|
||||
const replyClosureInfo: ClosureInfo = {
|
||||
resolution: "finalizat",
|
||||
reason: `Act administrativ emis — inchide ${parentEntry.number}`,
|
||||
closedBy: "",
|
||||
closedAt: new Date().toISOString(),
|
||||
hadActiveDeadlines: false,
|
||||
linkedEntryId: parentEntry.id,
|
||||
linkedEntryNumber: parentEntry.number,
|
||||
};
|
||||
await updateEntry(newEntry.id, {
|
||||
status: "inchis",
|
||||
closureInfo: replyClosureInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
setClosesEntryId(null);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ import { findAuthorityForContact } from "../services/authority-catalog";
|
||||
import { computeTransmissionStatus } from "../services/deadline-service";
|
||||
import { StatusMonitorConfig } from "./status-monitor-config";
|
||||
import { FlowDiagram } from "./flow-diagram";
|
||||
import { DeadlineTimeline } from "./deadline-timeline";
|
||||
|
||||
interface RegistryEntryDetailProps {
|
||||
entry: RegistryEntry | null;
|
||||
@@ -584,58 +585,10 @@ export function RegistryEntryDetail({
|
||||
</DetailSection>
|
||||
)}
|
||||
|
||||
{/* ── Legal deadlines ── */}
|
||||
{/* ── Legal deadlines (timeline view) ── */}
|
||||
{(entry.trackedDeadlines ?? []).length > 0 && (
|
||||
<DetailSection
|
||||
title={`Termene legale (${entry.trackedDeadlines!.length})`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{entry.trackedDeadlines!.map((dl) => (
|
||||
<div
|
||||
key={dl.id}
|
||||
className={cn(
|
||||
"rounded-md border px-3 py-2 text-xs",
|
||||
dl.resolution === "pending" &&
|
||||
new Date(dl.dueDate) < new Date()
|
||||
? "border-destructive/50 bg-destructive/5"
|
||||
: dl.resolution === "pending"
|
||||
? "border-amber-300 dark:border-amber-700 bg-amber-50/50 dark:bg-amber-950/20"
|
||||
: "border-muted",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{dl.typeId}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
dl.resolution === "pending" &&
|
||||
"border-amber-400 text-amber-700 dark:text-amber-300",
|
||||
dl.resolution === "completed" &&
|
||||
"border-green-400 text-green-700 dark:text-green-300",
|
||||
)}
|
||||
>
|
||||
{DEADLINE_RES_LABELS[dl.resolution] ?? dl.resolution}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-4 mt-1 text-muted-foreground">
|
||||
<span>
|
||||
<Calendar className="mr-0.5 inline h-2.5 w-2.5" />
|
||||
Start: {formatDate(dl.startDate)}
|
||||
</span>
|
||||
<span>
|
||||
<Clock className="mr-0.5 inline h-2.5 w-2.5" />
|
||||
Scadent: {formatDate(dl.dueDate)}
|
||||
</span>
|
||||
</div>
|
||||
{dl.resolutionNote && (
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{dl.resolutionNote}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DetailSection title="Termene legale">
|
||||
<DeadlineTimeline deadlines={entry.trackedDeadlines!} />
|
||||
</DetailSection>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { DeadlineCategory, DeadlineResolution } from '../types';
|
||||
|
||||
export interface DeadlineFilters {
|
||||
category: DeadlineCategory | 'all';
|
||||
resolution: DeadlineResolution | 'all';
|
||||
urgentOnly: boolean;
|
||||
}
|
||||
|
||||
export function useDeadlineFilters() {
|
||||
const [filters, setFilters] = useState<DeadlineFilters>({
|
||||
category: 'all',
|
||||
resolution: 'all',
|
||||
urgentOnly: false,
|
||||
});
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof DeadlineFilters>(key: K, value: DeadlineFilters[K]) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
setFilters({ category: 'all', resolution: 'all', urgentOnly: false });
|
||||
}, []);
|
||||
|
||||
return { filters, updateFilter, resetFilters };
|
||||
}
|
||||
@@ -275,14 +275,24 @@ export function groupDeadlinesByEntry(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are any user-selectable (non-autoTrack) deadlines
|
||||
const hasUserDeadlines = deadlines.some((dl) => {
|
||||
const d = getDeadlineType(dl.typeId);
|
||||
return !d?.autoTrack && !d?.backgroundOnly && !dl.chainParentId;
|
||||
});
|
||||
|
||||
// Second pass: build main deadline items
|
||||
for (const dl of deadlines) {
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
if (def?.backgroundOnly) continue;
|
||||
if (dl.chainParentId) continue; // chain children are nested
|
||||
|
||||
// Auto-tracked without chain parent: treat as main if pending, skip if resolved
|
||||
if (def?.autoTrack && dl.resolution !== "pending") continue;
|
||||
// Auto-tracked: hide when there are user-selectable deadlines (they appear as milestones)
|
||||
// Only show auto-tracked as standalone when there are NO user deadlines on the entry
|
||||
if (def?.autoTrack) {
|
||||
if (hasUserDeadlines) continue;
|
||||
if (dl.resolution !== "pending") continue;
|
||||
}
|
||||
|
||||
mainItems.push({
|
||||
deadline: dl,
|
||||
|
||||
Reference in New Issue
Block a user