feat: add deadline system guide/overview page (Ghid termene)
- New DeadlineConfigOverview component: read-only reference page showing all 18 deadline types organized by category with chain flow diagrams - Accessible via "Ghid termene" toggle in the "Termene legale" tab - Shows: summary stats, color legend, collapsible category sections, chain flow diagrams (fan-in + sequential), notification overview, document expiry settings, and legal reference index - All data derived from existing catalog (zero API calls) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,565 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Info,
|
||||||
|
Clock,
|
||||||
|
ShieldCheck,
|
||||||
|
Bell,
|
||||||
|
ArrowRight,
|
||||||
|
Scale,
|
||||||
|
FileWarning,
|
||||||
|
Timer,
|
||||||
|
Link2,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
|
import {
|
||||||
|
DEADLINE_CATALOG,
|
||||||
|
CATEGORY_LABELS,
|
||||||
|
getDeadlineType,
|
||||||
|
getDeadlinesByCategory,
|
||||||
|
} from "../services/deadline-catalog";
|
||||||
|
import { NOTIFICATION_TYPES } from "@/core/notifications/types";
|
||||||
|
import type { DeadlineCategory, DeadlineTypeDef } from "../types";
|
||||||
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
|
||||||
|
// ── Constants ──
|
||||||
|
|
||||||
|
const ALL_CATEGORIES: DeadlineCategory[] = [
|
||||||
|
"certificat",
|
||||||
|
"avize",
|
||||||
|
"completari",
|
||||||
|
"urbanism",
|
||||||
|
"autorizare",
|
||||||
|
"litigii",
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORY_ICONS: Record<DeadlineCategory, string> = {
|
||||||
|
certificat: "CU",
|
||||||
|
avize: "AV",
|
||||||
|
completari: "CO",
|
||||||
|
urbanism: "UR",
|
||||||
|
autorizare: "AC",
|
||||||
|
litigii: "LI",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Chain building ──
|
||||||
|
|
||||||
|
interface ChainGroup {
|
||||||
|
sources: DeadlineTypeDef[];
|
||||||
|
target: DeadlineTypeDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChainGroups(category: DeadlineCategory): ChainGroup[] {
|
||||||
|
const types = getDeadlinesByCategory(category);
|
||||||
|
const targetMap = new Map<string, DeadlineTypeDef[]>();
|
||||||
|
|
||||||
|
for (const t of types) {
|
||||||
|
if (t.chainNextTypeId) {
|
||||||
|
const sources = targetMap.get(t.chainNextTypeId) ?? [];
|
||||||
|
sources.push(t);
|
||||||
|
targetMap.set(t.chainNextTypeId, sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: ChainGroup[] = [];
|
||||||
|
for (const [targetId, sources] of targetMap) {
|
||||||
|
const target = getDeadlineType(targetId);
|
||||||
|
if (target) {
|
||||||
|
groups.push({ sources, target });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Legal reference index ──
|
||||||
|
|
||||||
|
function buildLegalIndex(): Map<string, DeadlineTypeDef[]> {
|
||||||
|
const index = new Map<string, DeadlineTypeDef[]>();
|
||||||
|
for (const t of DEADLINE_CATALOG) {
|
||||||
|
if (t.legalReference) {
|
||||||
|
const existing = index.get(t.legalReference) ?? [];
|
||||||
|
existing.push(t);
|
||||||
|
index.set(t.legalReference, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ──
|
||||||
|
|
||||||
|
export function DeadlineConfigOverview() {
|
||||||
|
const [expandedCategories, setExpandedCategories] = useState<Set<DeadlineCategory>>(
|
||||||
|
() => new Set(ALL_CATEGORIES),
|
||||||
|
);
|
||||||
|
const [expandedDescs, setExpandedDescs] = useState<Set<string>>(new Set());
|
||||||
|
const [showLegalIndex, setShowLegalIndex] = useState(false);
|
||||||
|
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = DEADLINE_CATALOG.length;
|
||||||
|
const autoTracked = DEADLINE_CATALOG.filter((d) => d.autoTrack).length;
|
||||||
|
const tacit = DEADLINE_CATALOG.filter((d) => d.tacitApprovalApplicable).length;
|
||||||
|
const chained = DEADLINE_CATALOG.filter((d) => d.chainNextTypeId).length;
|
||||||
|
return { total, autoTracked, tacit, chained };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const legalIndex = useMemo(() => buildLegalIndex(), []);
|
||||||
|
|
||||||
|
const toggleCategory = (cat: DeadlineCategory) => {
|
||||||
|
setExpandedCategories((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(cat)) next.delete(cat);
|
||||||
|
else next.add(cat);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDesc = (id: string) => {
|
||||||
|
setExpandedDescs((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* ── Summary stats ── */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
|
<StatCard icon={<Scale className="h-4 w-4" />} value={stats.total} label="Tipuri termene" />
|
||||||
|
<StatCard icon={<Zap className="h-4 w-4 text-purple-500" />} value={stats.autoTracked} label="Auto-tracked" />
|
||||||
|
<StatCard icon={<ShieldCheck className="h-4 w-4 text-blue-500" />} value={stats.tacit} label="Aprobare tacita" />
|
||||||
|
<StatCard icon={<Link2 className="h-4 w-4 text-amber-500" />} value={stats.chained} label="Termene in lant" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Legend ── */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2 rounded-lg border bg-muted/30 px-3 py-2">
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground">Legenda:</span>
|
||||||
|
<Badge variant="outline" className="bg-purple-100 text-purple-700 border-purple-300 text-[10px] dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700">
|
||||||
|
Auto
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-gray-100 text-gray-600 border-gray-300 text-[10px] dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600">
|
||||||
|
Manual
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300 text-[10px] dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700">
|
||||||
|
Tacit
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-green-100 text-green-700 border-green-300 text-[10px] dark:bg-green-900/30 dark:text-green-300 dark:border-green-700">
|
||||||
|
Lucratoare
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-orange-100 text-orange-700 border-orange-300 text-[10px] dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-700">
|
||||||
|
Calendaristice
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-dashed text-muted-foreground text-[10px]">
|
||||||
|
Fundal
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Category sections ── */}
|
||||||
|
{ALL_CATEGORIES.map((cat) => {
|
||||||
|
const types = getDeadlinesByCategory(cat);
|
||||||
|
if (types.length === 0) return null;
|
||||||
|
const isExpanded = expandedCategories.has(cat);
|
||||||
|
const chainGroups = buildChainGroups(cat);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={cat} className="rounded-lg border bg-card">
|
||||||
|
{/* Category header */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleCategory(cat)}
|
||||||
|
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-[10px] font-bold text-primary">
|
||||||
|
{CATEGORY_ICONS[cat]}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-sm font-medium">
|
||||||
|
{CATEGORY_LABELS[cat]}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{types.length} tip{types.length !== 1 ? "uri" : ""}
|
||||||
|
</Badge>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Category body */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div>
|
||||||
|
<Separator />
|
||||||
|
<div className="divide-y">
|
||||||
|
{types.map((t) => (
|
||||||
|
<DeadlineTypeRow
|
||||||
|
key={t.id}
|
||||||
|
type={t}
|
||||||
|
descExpanded={expandedDescs.has(t.id)}
|
||||||
|
onToggleDesc={() => toggleDesc(t.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chain flow diagrams */}
|
||||||
|
{chainGroups.length > 0 && (
|
||||||
|
<div className="border-t bg-muted/20 px-4 py-3">
|
||||||
|
<p className="mb-2 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Fluxuri de termene in lant
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{chainGroups.map((group) => (
|
||||||
|
<ChainFlowDiagram
|
||||||
|
key={group.target.id}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* ── Notification overview ── */}
|
||||||
|
<div className="rounded-lg border bg-card">
|
||||||
|
<div className="flex items-center gap-3 px-4 py-3">
|
||||||
|
<Bell className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium">Notificari email</span>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="px-4 py-3 space-y-3">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Digest zilnic trimis luni-vineri la 8:00 prin Brevo SMTP, declansat automat de N8N cron.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{NOTIFICATION_TYPES.map((nt) => (
|
||||||
|
<div key={nt.type} className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-medium">{nt.label}</span>
|
||||||
|
<span className="ml-2 text-xs text-muted-foreground">
|
||||||
|
{nt.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground italic">
|
||||||
|
Preferintele per utilizator se configureaza din iconita clopotel din bara de sus.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Document expiry overview ── */}
|
||||||
|
<div className="rounded-lg border bg-card">
|
||||||
|
<div className="flex items-center gap-3 px-4 py-3">
|
||||||
|
<FileWarning className="h-4 w-4 text-amber-500" />
|
||||||
|
<span className="text-sm font-medium">Expirare documente</span>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="px-4 py-3 space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-amber-500" />
|
||||||
|
<div className="text-xs">
|
||||||
|
<span className="font-medium">Certificat de Urbanism (CU)</span>
|
||||||
|
<span className="ml-1 text-muted-foreground">
|
||||||
|
— data expirare + fereastra de alerta (implicit 30 zile). Se configureaza per inregistrare.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-amber-500" />
|
||||||
|
<div className="text-xs">
|
||||||
|
<span className="font-medium">Autorizatie de Construire (AC)</span>
|
||||||
|
<span className="ml-1 text-muted-foreground">
|
||||||
|
— validitate 12 luni de la emitere, executie 6/12/24/36 luni, prelungire +24 luni.
|
||||||
|
Reminder lunar automat (snooze/dismiss disponibil).
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-blue-500" />
|
||||||
|
<div className="text-xs">
|
||||||
|
<span className="font-medium">Monitorizare externa</span>
|
||||||
|
<span className="ml-1 text-muted-foreground">
|
||||||
|
— verificare automata status la autoritate (Primaria Cluj etc.). Notificare la schimbare status.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground italic">
|
||||||
|
Transmiterea in termen (1 zi) se verifica automat in fundal cand un entry e inchis prin act administrativ.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Legal reference index ── */}
|
||||||
|
<div className="rounded-lg border bg-card">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLegalIndex(!showLegalIndex)}
|
||||||
|
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
|
<Scale className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="flex-1 text-sm font-medium">Index referinte legale</span>
|
||||||
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{legalIndex.size} ref.
|
||||||
|
</Badge>
|
||||||
|
{showLegalIndex ? (
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{showLegalIndex && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="px-4 py-3 space-y-2">
|
||||||
|
{Array.from(legalIndex.entries())
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
.map(([ref, types]) => (
|
||||||
|
<div key={ref} className="text-xs">
|
||||||
|
<span className="font-medium text-primary">{ref}</span>
|
||||||
|
<span className="ml-2 text-muted-foreground">
|
||||||
|
{types.map((t) => t.label).join(", ")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stat card ──
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
icon,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 rounded-lg border bg-card px-3 py-2.5">
|
||||||
|
{icon}
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-semibold leading-none">{value}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">{label}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Deadline type row ──
|
||||||
|
|
||||||
|
function DeadlineTypeRow({
|
||||||
|
type,
|
||||||
|
descExpanded,
|
||||||
|
onToggleDesc,
|
||||||
|
}: {
|
||||||
|
type: DeadlineTypeDef;
|
||||||
|
descExpanded: boolean;
|
||||||
|
onToggleDesc: () => void;
|
||||||
|
}) {
|
||||||
|
const chainTarget = type.chainNextTypeId
|
||||||
|
? getDeadlineType(type.chainNextTypeId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-2.5",
|
||||||
|
type.backgroundOnly && "opacity-60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{/* Label */}
|
||||||
|
<span className="text-xs font-medium">{type.label}</span>
|
||||||
|
|
||||||
|
{/* Duration badge */}
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"text-[10px]",
|
||||||
|
type.dayType === "working"
|
||||||
|
? "bg-green-50 text-green-700 border-green-300 dark:bg-green-900/20 dark:text-green-300 dark:border-green-700"
|
||||||
|
: "bg-orange-50 text-orange-700 border-orange-300 dark:bg-orange-900/20 dark:text-orange-300 dark:border-orange-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Timer className="mr-0.5 inline h-2.5 w-2.5" />
|
||||||
|
{type.days}z {type.dayType === "working" ? "lucr." : "cal."}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{/* Auto/Manual badge */}
|
||||||
|
{type.autoTrack ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-purple-50 text-purple-700 border-purple-300 text-[10px] dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-700"
|
||||||
|
>
|
||||||
|
Auto
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="text-[10px] text-muted-foreground">
|
||||||
|
Manual
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tacit badge */}
|
||||||
|
{type.tacitApprovalApplicable && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-blue-50 text-blue-700 border-blue-300 text-[10px] dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-700"
|
||||||
|
>
|
||||||
|
<ShieldCheck className="mr-0.5 inline h-2.5 w-2.5" />
|
||||||
|
Tacit
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Background badge */}
|
||||||
|
{type.backgroundOnly && (
|
||||||
|
<Badge variant="outline" className="border-dashed text-[10px] text-muted-foreground">
|
||||||
|
Fundal
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Direction filter */}
|
||||||
|
{type.directionFilter && type.directionFilter.length === 1 && (
|
||||||
|
<Badge variant="outline" className="text-[10px] text-muted-foreground">
|
||||||
|
{type.directionFilter[0] === "iesit" ? "Iesit" : "Intrat"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info toggle */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleDesc}
|
||||||
|
className="ml-auto shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
|
title="Detalii"
|
||||||
|
>
|
||||||
|
<Info className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chain indicator */}
|
||||||
|
{chainTarget && (
|
||||||
|
<div className="mt-1 flex items-center gap-1.5 text-[10px] text-amber-600 dark:text-amber-400">
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
<span>
|
||||||
|
La rezolvare lanseaza: <span className="font-medium">{chainTarget.label}</span>
|
||||||
|
{" "}({chainTarget.days}z {chainTarget.dayType === "working" ? "lucr." : "cal."})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description + legal ref (expanded) */}
|
||||||
|
{descExpanded && (
|
||||||
|
<div className="mt-2 space-y-1 rounded-md bg-muted/30 px-3 py-2">
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||||
|
{type.description}
|
||||||
|
</p>
|
||||||
|
{type.legalReference && (
|
||||||
|
<p className="text-[10px] text-primary/70">
|
||||||
|
<Scale className="mr-0.5 inline h-2.5 w-2.5" />
|
||||||
|
{type.legalReference}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{type.startDateHint && (
|
||||||
|
<p className="text-[10px] text-muted-foreground italic">
|
||||||
|
<Clock className="mr-0.5 inline h-2.5 w-2.5" />
|
||||||
|
{type.startDateHint}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chain flow diagram ──
|
||||||
|
|
||||||
|
function ChainFlowDiagram({ group }: { group: ChainGroup }) {
|
||||||
|
const isFanIn = group.sources.length > 1;
|
||||||
|
|
||||||
|
if (isFanIn) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-0">
|
||||||
|
{/* Multiple sources stacked */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{group.sources.map((src) => (
|
||||||
|
<ChainNode key={src.id} type={src} small />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Converging arrows */}
|
||||||
|
<div className="flex items-center px-1">
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
{group.sources.map((_, i) => (
|
||||||
|
<div key={i} className="h-px w-4 bg-amber-400/60" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="h-px w-3 bg-amber-400" />
|
||||||
|
<div className="border-y-[4px] border-l-[6px] border-y-transparent border-l-amber-400" />
|
||||||
|
</div>
|
||||||
|
{/* Target */}
|
||||||
|
<ChainNode type={group.target} isTarget />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single chain: source → target
|
||||||
|
const src = group.sources[0];
|
||||||
|
if (!src) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-0">
|
||||||
|
<ChainNode type={src} />
|
||||||
|
<div className="flex items-center px-1">
|
||||||
|
<div className="h-px w-6 bg-amber-400" />
|
||||||
|
<div className="border-y-[4px] border-l-[6px] border-y-transparent border-l-amber-400" />
|
||||||
|
</div>
|
||||||
|
<ChainNode type={group.target} isTarget />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChainNode({
|
||||||
|
type,
|
||||||
|
isTarget,
|
||||||
|
small,
|
||||||
|
}: {
|
||||||
|
type: DeadlineTypeDef;
|
||||||
|
isTarget?: boolean;
|
||||||
|
small?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border px-2 py-1",
|
||||||
|
small ? "max-w-[160px]" : "max-w-[200px]",
|
||||||
|
isTarget
|
||||||
|
? "border-amber-400 bg-amber-50/50 dark:bg-amber-950/20 dark:border-amber-700"
|
||||||
|
: "border-border bg-background",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className={cn("font-medium leading-tight", small ? "text-[9px]" : "text-[10px]")}>
|
||||||
|
{type.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] text-muted-foreground">
|
||||||
|
{type.days}z {type.dayType === "working" ? "lucr." : "cal."}
|
||||||
|
{type.tacitApprovalApplicable && (
|
||||||
|
<span className="ml-1 text-blue-600 dark:text-blue-400">tacit</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ import { RegistryTable } from "./registry-table";
|
|||||||
import { RegistryEntryForm } from "./registry-entry-form";
|
import { RegistryEntryForm } from "./registry-entry-form";
|
||||||
import { RegistryEntryDetail } from "./registry-entry-detail";
|
import { RegistryEntryDetail } from "./registry-entry-detail";
|
||||||
import { DeadlineDashboard } from "./deadline-dashboard";
|
import { DeadlineDashboard } from "./deadline-dashboard";
|
||||||
|
import { DeadlineConfigOverview } from "./deadline-config-overview";
|
||||||
import { ThreadExplorer } from "./thread-explorer";
|
import { ThreadExplorer } from "./thread-explorer";
|
||||||
import { CloseGuardDialog } from "./close-guard-dialog";
|
import { CloseGuardDialog } from "./close-guard-dialog";
|
||||||
import { getOverdueDays } from "../services/registry-service";
|
import { getOverdueDays } from "../services/registry-service";
|
||||||
@@ -81,6 +82,7 @@ export function RegistraturaModule() {
|
|||||||
const [conexToEntry, setConexToEntry] = useState<RegistryEntry | null>(null);
|
const [conexToEntry, setConexToEntry] = useState<RegistryEntry | null>(null);
|
||||||
/** If set, the parent entry will be closed after saving the new reply entry */
|
/** If set, the parent entry will be closed after saving the new reply entry */
|
||||||
const [closesEntryId, setClosesEntryId] = useState<string | null>(null);
|
const [closesEntryId, setClosesEntryId] = useState<string | null>(null);
|
||||||
|
const [termeneView, setTermeneView] = useState<"active" | "guide">("active");
|
||||||
const [closingId, setClosingId] = useState<string | null>(null);
|
const [closingId, setClosingId] = useState<string | null>(null);
|
||||||
const [linkCheckId, setLinkCheckId] = useState<string | null>(null);
|
const [linkCheckId, setLinkCheckId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -630,11 +632,34 @@ export function RegistraturaModule() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="termene">
|
<TabsContent value="termene">
|
||||||
<DeadlineDashboard
|
{/* Sub-navigation: active deadlines vs. system guide */}
|
||||||
entries={allEntries}
|
<div className="mb-4 flex items-center gap-2">
|
||||||
onResolveDeadline={handleDashboardResolve}
|
<Button
|
||||||
onAddChainedDeadline={handleAddChainedDeadline}
|
variant={termeneView === "active" ? "default" : "outline"}
|
||||||
/>
|
size="sm"
|
||||||
|
onClick={() => setTermeneView("active")}
|
||||||
|
>
|
||||||
|
Termene active
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={termeneView === "guide" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTermeneView("guide")}
|
||||||
|
>
|
||||||
|
<BookOpen className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
Ghid termene
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{termeneView === "active" ? (
|
||||||
|
<DeadlineDashboard
|
||||||
|
entries={allEntries}
|
||||||
|
onResolveDeadline={handleDashboardResolve}
|
||||||
|
onAddChainedDeadline={handleAddChainedDeadline}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DeadlineConfigOverview />
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user