442a1565fd
- Add "Necesita analiza in comisie" toggle for avize category (mirrors CJ toggle)
- When OFF: auto-creates 5-day working limit for completari requests
- When ON: no limit (institution can request completions anytime)
- Add interruption mechanism: resolve aviz as "intrerupt" when institution
requests completions → auto-creates new 15-day deadline from completions date
- New resolution type "intrerupt" with yellow badge + chain support
- Restructure avize catalog entries:
- aviz-ac-15 (L50) and aviz-urbanism-30 (L350) now have chain to
aviz-emitere-dupa-completari for interruption flow
- aviz-mediu: updated hints about procedure closure prerequisite
- aviz-cultura-comisie: 2-phase with auto-track depunere la comisie (30 days)
- aeronautica, ISU, transport-eu: all get interruption chain
- 3 new auto-track entries: aviz-completari-limit (5zl), aviz-emitere-dupa-completari
(15zc), aviz-cultura-depunere-comisie (30zc)
- New document type: "Convocare sedinta"
- Info boxes in dialog explaining auto-track behavior + interruption mechanism
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
216 lines
7.4 KiB
TypeScript
216 lines
7.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Clock, CheckCircle2, X, History, ShieldCheck, Building2 } 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);
|
|
const [showAudit, setShowAudit] = useState(false);
|
|
const auditLog = deadline.auditLog ?? [];
|
|
|
|
const isAutoTrack = def?.autoTrack === true;
|
|
const isVerificationDeadline = deadline.typeId === "cu-verificare";
|
|
const isCJDeadline =
|
|
deadline.typeId === "cu-cj-solicitare-aviz" ||
|
|
deadline.typeId === "cu-cj-aviz-primar";
|
|
|
|
// For verification deadlines, check if the 10-day period has passed
|
|
const verificationExpired =
|
|
isVerificationDeadline &&
|
|
deadline.resolution === "pending" &&
|
|
status.daysRemaining !== null &&
|
|
status.daysRemaining < 0;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"rounded-lg border p-3",
|
|
VARIANT_CLASSES[status.variant] ?? "",
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{isAutoTrack ? (
|
|
isCJDeadline ? (
|
|
<Building2 className="h-4 w-4 shrink-0 text-amber-500" />
|
|
) : (
|
|
<ShieldCheck className="h-4 w-4 shrink-0 text-blue-500" />
|
|
)
|
|
) : (
|
|
<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 flex-wrap">
|
|
<span className="text-sm font-medium truncate">
|
|
{def?.label ?? deadline.typeId}
|
|
</span>
|
|
{isAutoTrack && (
|
|
<Badge variant="outline" className="text-[10px] text-purple-600 border-purple-300">
|
|
auto
|
|
</Badge>
|
|
)}
|
|
<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 depasit`
|
|
: `${status.daysRemaining}z`}
|
|
)
|
|
</span>
|
|
)}
|
|
</Badge>
|
|
</div>
|
|
{/* Verification badge: institution lost right to return docs */}
|
|
{verificationExpired && (
|
|
<div className="flex items-center gap-1 mt-1">
|
|
<Badge className="text-[10px] border-0 bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200">
|
|
<ShieldCheck className="h-3 w-3 mr-0.5" />
|
|
Nu mai pot returna documentatia
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
{/* Interruption badge */}
|
|
{deadline.resolution === "intrerupt" && (
|
|
<div className="flex items-center gap-1 mt-1">
|
|
<Badge className="text-[10px] border-0 bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200">
|
|
Intrerupt — completari solicitate
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
<div className="text-xs text-muted-foreground mt-0.5">
|
|
{def?.isBackwardDeadline ? "Termen limita" : "Start"}:{" "}
|
|
{formatDate(deadline.startDate)}
|
|
{" \u2192 "}
|
|
{def?.isBackwardDeadline ? "Depunere pana la" : "Termen"}:{" "}
|
|
{formatDate(deadline.dueDate)}
|
|
{def?.dayType === "working" && (
|
|
<span className="ml-1">(zile lucratoare)</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<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-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",
|
|
});
|
|
} 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;
|
|
}
|
|
}
|