feat(registratura): add legal deadline tracking system (Termene Legale)
Full deadline tracking engine for Romanian construction permitting: - 16 deadline types across 5 categories (Avize, Completări, Analiză, Autorizare, Publicitate) - Working days vs calendar days with Romanian public holidays (Orthodox Easter via Meeus) - Backward deadlines (AC extension: 45 working days BEFORE expiry) - Chain deadlines (resolving one prompts adding the next) - Tacit approval auto-detection (overdue + applicable type) - Tabbed UI: Registru + Termene legale dashboard with stats/filters/table - Inline deadline cards in entry form with add/resolve/remove - Clock icon + count badge on registry table for entries with deadlines Also adds CLAUDE.md with full project context for AI assistant handoff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import { Paperclip, X } from 'lucide-react';
|
||||
import { Paperclip, X, Clock, Plus } from 'lucide-react';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, RegistryAttachment } from '../types';
|
||||
import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, RegistryAttachment, TrackedDeadline, DeadlineResolution } from '../types';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Textarea } from '@/shared/components/ui/textarea';
|
||||
@@ -12,6 +12,11 @@ import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
||||
import { useContacts } from '@/modules/address-book/hooks/use-contacts';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DeadlineCard } from './deadline-card';
|
||||
import { DeadlineAddDialog } from './deadline-add-dialog';
|
||||
import { DeadlineResolveDialog } from './deadline-resolve-dialog';
|
||||
import { createTrackedDeadline, resolveDeadline as resolveDeadlineFn } from '../services/deadline-service';
|
||||
import { getDeadlineType } from '../services/deadline-catalog';
|
||||
|
||||
interface RegistryEntryFormProps {
|
||||
initial?: RegistryEntry;
|
||||
@@ -50,6 +55,39 @@ export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: R
|
||||
const [notes, setNotes] = useState(initial?.notes ?? '');
|
||||
const [linkedEntryIds, setLinkedEntryIds] = useState<string[]>(initial?.linkedEntryIds ?? []);
|
||||
const [attachments, setAttachments] = useState<RegistryAttachment[]>(initial?.attachments ?? []);
|
||||
const [trackedDeadlines, setTrackedDeadlines] = useState<TrackedDeadline[]>(initial?.trackedDeadlines ?? []);
|
||||
|
||||
// ── Deadline dialogs ──
|
||||
const [deadlineAddOpen, setDeadlineAddOpen] = useState(false);
|
||||
const [resolvingDeadline, setResolvingDeadline] = useState<TrackedDeadline | null>(null);
|
||||
|
||||
const handleAddDeadline = (typeId: string, startDate: string, chainParentId?: string) => {
|
||||
const tracked = createTrackedDeadline(typeId, startDate, chainParentId);
|
||||
if (tracked) setTrackedDeadlines((prev) => [...prev, tracked]);
|
||||
};
|
||||
|
||||
const handleResolveDeadline = (resolution: DeadlineResolution, note: string, chainNext: boolean) => {
|
||||
if (!resolvingDeadline) return;
|
||||
const resolved = resolveDeadlineFn(resolvingDeadline, resolution, note);
|
||||
setTrackedDeadlines((prev) =>
|
||||
prev.map((d) => (d.id === resolved.id ? resolved : d))
|
||||
);
|
||||
|
||||
// Handle chain
|
||||
if (chainNext) {
|
||||
const def = getDeadlineType(resolvingDeadline.typeId);
|
||||
if (def?.chainNextTypeId) {
|
||||
const resolvedDate = new Date().toISOString().slice(0, 10);
|
||||
const chained = createTrackedDeadline(def.chainNextTypeId, resolvedDate, resolvingDeadline.id);
|
||||
if (chained) setTrackedDeadlines((prev) => [...prev, chained]);
|
||||
}
|
||||
}
|
||||
setResolvingDeadline(null);
|
||||
};
|
||||
|
||||
const handleRemoveDeadline = (deadlineId: string) => {
|
||||
setTrackedDeadlines((prev) => prev.filter((d) => d.id !== deadlineId));
|
||||
};
|
||||
|
||||
// ── Sender/Recipient autocomplete suggestions ──
|
||||
const [senderFocused, setSenderFocused] = useState(false);
|
||||
@@ -111,6 +149,7 @@ export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: R
|
||||
deadline: deadline || undefined,
|
||||
linkedEntryIds,
|
||||
attachments,
|
||||
trackedDeadlines: trackedDeadlines.length > 0 ? trackedDeadlines : undefined,
|
||||
notes,
|
||||
tags: initial?.tags ?? [],
|
||||
visibility: initial?.visibility ?? 'all',
|
||||
@@ -278,6 +317,50 @@ export function RegistryEntryForm({ initial, allEntries, onSubmit, onCancel }: R
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tracked Deadlines */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Termene legale
|
||||
</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setDeadlineAddOpen(true)}>
|
||||
<Plus className="mr-1 h-3.5 w-3.5" /> Adaugă termen
|
||||
</Button>
|
||||
</div>
|
||||
{trackedDeadlines.length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{trackedDeadlines.map((dl) => (
|
||||
<DeadlineCard
|
||||
key={dl.id}
|
||||
deadline={dl}
|
||||
onResolve={setResolvingDeadline}
|
||||
onRemove={handleRemoveDeadline}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{trackedDeadlines.length === 0 && (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Niciun termen legal. Apăsați "Adaugă termen" pentru a urmări un termen.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeadlineAddDialog
|
||||
open={deadlineAddOpen}
|
||||
onOpenChange={setDeadlineAddOpen}
|
||||
entryDate={date}
|
||||
onAdd={handleAddDeadline}
|
||||
/>
|
||||
|
||||
<DeadlineResolveDialog
|
||||
open={resolvingDeadline !== null}
|
||||
deadline={resolvingDeadline}
|
||||
onOpenChange={(open) => { if (!open) setResolvingDeadline(null); }}
|
||||
onResolve={handleResolveDeadline}
|
||||
/>
|
||||
|
||||
{/* Attachments */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user