diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e5f6a64 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,266 @@ +# ArchiTools — Project Context for AI Assistants + +> This file provides all context needed for Claude Code, Sonnet, or any AI model to work on this project from scratch. + +--- + +## Quick Start + +```bash +npm install +npm run dev # http://localhost:3000 +npx next build # verify zero errors before pushing +git push origin main # auto-deploys via Portainer webhook +``` + +--- + +## Project Overview + +**ArchiTools** is a modular internal web dashboard for an architecture/engineering office group of 3 companies: +- **Beletage** (architecture) +- **Urban Switch** (urbanism) +- **Studii de Teren** (geotechnics) + +It runs on an on-premise Ubuntu server at `10.10.10.166`, containerized with Docker, managed via Portainer, served by Nginx Proxy Manager. + +### Stack +| Layer | Technology | +|---|---| +| Framework | Next.js 16.x, App Router, TypeScript (strict) | +| Styling | Tailwind CSS v4, shadcn/ui | +| State | localStorage (via StorageService abstraction) | +| Deploy | Docker multi-stage, Portainer, Nginx Proxy Manager | +| Repo | Gitea at `http://10.10.10.166:3002/gitadmin/ArchiTools` | +| Language | Code in **English**, UI in **Romanian** | + +### Architecture Principles +- **Module platform, not monolith** — each module isolated with own types/services/hooks/components +- **Feature flags** gate module loading (disabled = zero bundle cost) +- **Storage abstraction**: `StorageService` interface with adapters (localStorage default, designed for future DB/MinIO) +- **Cross-module tagging system** as shared service +- **Auth stub** designed for future Authentik SSO integration +- **All entities** include `visibility` / `createdBy` fields from day one + +--- + +## Repository Structure + +``` +src/ +├── app/ # Routing only (thin wrappers) +│ ├── (modules)/ # Module route pages +│ └── layout.tsx # App shell +├── core/ # Platform services +│ ├── module-registry/ # Module registration + types +│ ├── feature-flags/ # Flag evaluation + env override +│ ├── storage/ # StorageService + adapters +│ │ └── adapters/ # localStorage adapter (+ future DB/MinIO) +│ ├── tagging/ # Cross-module tag service +│ ├── i18n/ # Romanian translations +│ ├── theme/ # Light/dark theme +│ └── auth/ # Auth types + stub (future Authentik) +├── modules/ # Module business logic +│ ├── / +│ │ ├── components/ # Module UI components +│ │ ├── hooks/ # Module-specific hooks +│ │ ├── services/ # Module business logic +│ │ ├── types.ts # Module types +│ │ ├── config.ts # Module metadata +│ │ └── index.ts # Public exports +│ └── ... +├── shared/ # Shared UI +│ ├── components/ +│ │ ├── ui/ # shadcn/ui primitives +│ │ ├── layout/ # Sidebar, Header +│ │ └── common/ # Reusable app components +│ ├── hooks/ # Shared hooks +│ └── lib/ # Utils (cn, etc.) +├── config/ # Global config +│ ├── modules.ts # Module registry entries +│ ├── flags.ts # Default feature flags +│ ├── navigation.ts # Sidebar nav structure +│ └── companies.ts # Company definitions +docs/ # 16 internal technical docs +legacy/ # Original HTML tools for reference +``` + +--- + +## Implemented Modules (13/13 — zero placeholders) + +| # | Module | Route | Key Features | +|---|---|---|---| +| 1 | **Dashboard** | `/` | Stats cards, module grid, external tools by category | +| 2 | **Email Signature** | `/email-signature` | Multi-company branding, live preview, zoom/copy/download | +| 3 | **Word XML Generator** | `/word-xml` | Category-based XML gen, simple/advanced mode, ZIP export | +| 4 | **Registratura** | `/registratura` | CRUD registry, stats, filters, **legal deadline tracking** | +| 5 | **Tag Manager** | `/tag-manager` | CRUD tags, category/scope/color, grouped display | +| 6 | **IT Inventory** | `/it-inventory` | Equipment tracking, type/status/company filters | +| 7 | **Address Book** | `/address-book` | CRUD contacts, card grid, search/type filter | +| 8 | **Password Vault** | `/password-vault` | CRUD credentials, show/hide/copy, category filter | +| 9 | **Mini Utilities** | `/mini-utilities` | Text case, char counter, percentage calc, area converter | +| 10 | **Prompt Generator** | `/prompt-generator` | Template-driven prompt builder, 4 builtin templates | +| 11 | **Digital Signatures** | `/digital-signatures` | CRUD signature/stamp/initials assets | +| 12 | **Word Templates** | `/word-templates` | Template library, 8 categories, version tracking | +| 13 | **AI Chat** | `/ai-chat` | Session-based chat UI, demo mode (no API keys yet) | + +### Registratura — Legal Deadline Tracking (Termene Legale) + +The Registratura module includes a full legal 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 holiday support (including Orthodox Easter via Meeus algorithm) +- **Backward deadlines** (e.g., AC extension: 45 working days BEFORE expiry) +- **Chain deadlines** (resolving one prompts adding the next) +- **Tacit approval** (auto-detected when overdue + applicable type) +- **Tabbed UI**: "Registru" tab (existing registry) + "Termene legale" tab (deadline dashboard) + +Key files: +- `services/working-days.ts` — Romanian holidays, `addWorkingDays()`, `isWorkingDay()` +- `services/deadline-catalog.ts` — 16 `DeadlineTypeDef` entries +- `services/deadline-service.ts` — `createTrackedDeadline()`, `resolveDeadline()`, `aggregateDeadlines()` +- `components/deadline-dashboard.tsx` — Stats + filters + table +- `components/deadline-add-dialog.tsx` — 3-step wizard (category → type → date preview) + +--- + +## Infrastructure + +### Server: `10.10.10.166` (Ubuntu) + +| Service | Port | Purpose | +|---|---|---| +| **ArchiTools** | 3000 | This app | +| **Gitea** | 3002 | Git hosting (`gitadmin/ArchiTools`) | +| **Portainer** | 9000 | Docker management, auto-deploy on push | +| **Nginx Proxy Manager** | 81 (admin) | Reverse proxy + SSL termination | +| **Uptime Kuma** | 3001 | Service monitoring | +| **MinIO** | 9003 | Object storage (future) | +| **N8N** | 5678 | Workflow automation (future) | +| **Stirling PDF** | 8087 | PDF tools | +| **IT-Tools** | 8085 | Developer utilities | +| **FileBrowser** | 8086 | File management | +| **Netdata** | 19999 | System monitoring | +| **Dozzle** | 9999 | Docker log viewer | +| **CrowdSec** | 8088 | Security | +| **Authentik** | 9100 | SSO (future) | + +### Deployment Pipeline + +``` +git push origin main + → Gitea webhook fires + → Portainer auto-redeploys stack + → Docker multi-stage build (~1-2 min) + → Container starts on :3000 + → Nginx Proxy Manager routes traffic +``` + +### Docker +- `Dockerfile`: 3-stage build (deps → builder → runner), `node:20-alpine`, non-root user +- `docker-compose.yml`: single service, port 3000, watchtower label +- `output: 'standalone'` in `next.config.ts` is **required** + +--- + +## Development Rules + +### TypeScript Strict Mode Gotchas +- `array.split()[0]` returns `string | undefined` — use `.slice(0, 10)` instead +- `Record[key]` returns `T | undefined` — always guard with null check +- Spread of possibly-undefined objects: `{ ...obj[key], field }` — check existence first +- lucide-react Icons: cast through `unknown` → `React.ComponentType<{ className?: string }>` + +### Conventions +- **Code**: English +- **UI text**: Romanian +- **Components**: functional, `'use client'` directive where needed +- **State**: localStorage via `useStorage('module-name')` hook +- **IDs**: `uuid v4` +- **Dates**: ISO strings (`YYYY-MM-DD` for display, full ISO for timestamps) +- **No emojis** in code or UI unless explicitly requested + +### Module Development Pattern +Every module follows: +``` +src/modules// +├── components/ # React components +├── hooks/ # Custom hooks (use-.ts) +├── services/ # Business logic (pure functions) +├── types.ts # TypeScript interfaces +├── config.ts # ModuleConfig metadata +└── index.ts # Public exports +``` + +### Before Pushing +1. `npx next build` — must pass with zero errors +2. Test the feature manually on `localhost:3000` +3. Commit with descriptive message +4. `git push origin main` — Portainer auto-deploys + +--- + +## Company IDs + +| ID | Name | Prefix | +|---|---|---| +| `beletage` | Beletage | B | +| `urban-switch` | Urban Switch | US | +| `studii-de-teren` | Studii de Teren | SDT | +| `group` | Grup | G | + +--- + +## Future Integrations (not yet implemented) + +| Feature | Status | Notes | +|---|---|---| +| **Authentik SSO** | Auth stub exists | `src/core/auth/` has types + provider shell | +| **MinIO storage** | Adapter pattern ready | Switch `NEXT_PUBLIC_STORAGE_ADAPTER` to `minio` | +| **API backend** | Adapter pattern ready | Switch to `api` adapter when backend exists | +| **AI Chat API** | UI complete, demo mode | No API keys yet; supports Claude/GPT/Ollama | +| **N8N automations** | Webhook URL configured | For notifications, backups, workflows | + +--- + +## Model Recommendations for Tasks + +| Task Type | Recommended Model | Why | +|---|---|---| +| **Bug fixes, small edits** | Sonnet 4.5 or Haiku 4.5 | Fast, cheap, good for focused changes | +| **New module implementation** | Opus 4.6 | Complex architecture decisions, multi-file coordination | +| **Refactoring** | Sonnet 4.5 | Good pattern recognition, thorough but faster than Opus | +| **UI polish / styling** | Sonnet 4.5 | Tailwind expertise, fast iteration | +| **Documentation** | Sonnet 4.5 | Clear writing, fast output | +| **Complex business logic** | Opus 4.6 | Legal deadline logic, calendar math, chain workflows | +| **Build/deploy debugging** | Haiku 4.5 | Quick diagnosis, low cost | + +### Session Handoff Tips +- Read this `CLAUDE.md` first — it has all context +- Check `docs/` for deep dives on specific systems +- Check `src/modules//types.ts` before modifying any module +- Always run `npx next build` before committing +- The 16 docs in `docs/` total ~10,600 lines — search them for architecture questions + +--- + +## Documentation Index + +| Doc | Path | Content | +|---|---|---| +| System Architecture | `docs/architecture/SYSTEM-ARCHITECTURE.md` | Overall architecture, module platform design | +| Module System | `docs/architecture/MODULE-SYSTEM.md` | Module registry, lifecycle, config format | +| Feature Flags | `docs/architecture/FEATURE-FLAGS.md` | Flag system, env overrides | +| Storage Layer | `docs/architecture/STORAGE-LAYER.md` | StorageService interface, adapters | +| Tagging System | `docs/architecture/TAGGING-SYSTEM.md` | Cross-module tags | +| Security & Roles | `docs/architecture/SECURITY-AND-ROLES.md` | Visibility, auth, roles | +| Module Dev Guide | `docs/guides/MODULE-DEVELOPMENT.md` | How to create a new module | +| HTML Integration | `docs/guides/HTML-TOOL-INTEGRATION.md` | Legacy tool migration | +| UI Design System | `docs/guides/UI-DESIGN-SYSTEM.md` | Design tokens, component patterns | +| Docker Deployment | `docs/guides/DOCKER-DEPLOYMENT.md` | Full Docker/Portainer/Nginx guide | +| Coding Standards | `docs/guides/CODING-STANDARDS.md` | TS strict, naming, patterns | +| Testing Strategy | `docs/guides/TESTING-STRATEGY.md` | Testing approach | +| Configuration | `docs/guides/CONFIGURATION.md` | Env vars, flags, companies | +| Data Model | `docs/DATA-MODEL.md` | All entity schemas | +| Repo Structure | `docs/REPO-STRUCTURE.md` | Directory layout | +| Prompt Generator | `docs/modules/PROMPT-GENERATOR.md` | Prompt module deep dive | diff --git a/src/modules/registratura/components/deadline-add-dialog.tsx b/src/modules/registratura/components/deadline-add-dialog.tsx new file mode 100644 index 0000000..2f51d41 --- /dev/null +++ b/src/modules/registratura/components/deadline-add-dialog.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/shared/components/ui/dialog'; +import { Button } from '@/shared/components/ui/button'; +import { Input } from '@/shared/components/ui/input'; +import { Label } from '@/shared/components/ui/label'; +import { Badge } from '@/shared/components/ui/badge'; +import { DEADLINE_CATALOG, CATEGORY_LABELS } from '../services/deadline-catalog'; +import { computeDueDate } from '../services/working-days'; +import type { DeadlineCategory, DeadlineTypeDef } from '../types'; + +interface DeadlineAddDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + entryDate: string; + onAdd: (typeId: string, startDate: string) => void; +} + +type Step = 'category' | 'type' | 'date'; + +const CATEGORIES: DeadlineCategory[] = ['avize', 'completari', 'analiza', 'autorizare', 'publicitate']; + +export function DeadlineAddDialog({ open, onOpenChange, entryDate, onAdd }: DeadlineAddDialogProps) { + const [step, setStep] = useState('category'); + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedType, setSelectedType] = useState(null); + const [startDate, setStartDate] = useState(entryDate); + + const typesForCategory = useMemo(() => { + if (!selectedCategory) return []; + return DEADLINE_CATALOG.filter((d) => d.category === selectedCategory); + }, [selectedCategory]); + + const dueDatePreview = useMemo(() => { + if (!selectedType || !startDate) return null; + const start = new Date(startDate); + start.setHours(0, 0, 0, 0); + const due = computeDueDate(start, selectedType.days, selectedType.dayType, selectedType.isBackwardDeadline); + return due.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }); + }, [selectedType, startDate]); + + const handleClose = () => { + setStep('category'); + setSelectedCategory(null); + setSelectedType(null); + setStartDate(entryDate); + onOpenChange(false); + }; + + const handleCategorySelect = (cat: DeadlineCategory) => { + setSelectedCategory(cat); + setStep('type'); + }; + + const handleTypeSelect = (typ: DeadlineTypeDef) => { + setSelectedType(typ); + if (!typ.requiresCustomStartDate) { + setStartDate(entryDate); + } + setStep('date'); + }; + + const handleBack = () => { + if (step === 'type') { + setStep('category'); + setSelectedCategory(null); + } else if (step === 'date') { + setStep('type'); + setSelectedType(null); + } + }; + + const handleConfirm = () => { + if (!selectedType || !startDate) return; + onAdd(selectedType.id, startDate); + handleClose(); + }; + + return ( + { if (!o) handleClose(); }}> + + + + {step === 'category' && 'Adaugă termen legal — Categorie'} + {step === 'type' && `Adaugă termen legal — ${selectedCategory ? CATEGORY_LABELS[selectedCategory] : ''}`} + {step === 'date' && `Adaugă termen legal — ${selectedType?.label ?? ''}`} + + + + {step === 'category' && ( +
+ {CATEGORIES.map((cat) => ( + + ))} +
+ )} + + {step === 'type' && ( +
+ {typesForCategory.map((typ) => ( + + ))} +
+ )} + + {step === 'date' && selectedType && ( +
+
+ + {selectedType.startDateHint && ( +

{selectedType.startDateHint}

+ )} + setStartDate(e.target.value)} + className="mt-1" + /> +
+ + {dueDatePreview && ( +
+

+ {selectedType.isBackwardDeadline ? 'Termen limită depunere' : 'Termen limită calculat'} +

+

{dueDatePreview}

+

+ {selectedType.days} {selectedType.dayType === 'working' ? 'zile lucrătoare' : 'zile calendaristice'} + {selectedType.isBackwardDeadline ? ' ÎNAINTE' : ' de la data start'} +

+ {selectedType.legalReference && ( +

Ref: {selectedType.legalReference}

+ )} +
+ )} +
+ )} + + + {step !== 'category' && ( + + )} + {step === 'category' && ( + + )} + {step === 'date' && ( + + )} + +
+
+ ); +} diff --git a/src/modules/registratura/components/deadline-card.tsx b/src/modules/registratura/components/deadline-card.tsx new file mode 100644 index 0000000..d71d3c1 --- /dev/null +++ b/src/modules/registratura/components/deadline-card.tsx @@ -0,0 +1,93 @@ +'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'; + +interface DeadlineCardProps { + deadline: TrackedDeadline; + onResolve: (deadline: TrackedDeadline) => void; + onRemove: (deadlineId: string) => void; +} + +const VARIANT_CLASSES: Record = { + 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 = { + 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); + + return ( +
+ +
+
+ {def?.label ?? deadline.typeId} + + {status.label} + {status.daysRemaining !== null && status.variant !== 'blue' && ( + + ({status.daysRemaining < 0 ? `${Math.abs(status.daysRemaining)}z depășit` : `${status.daysRemaining}z`}) + + )} + +
+
+ {def?.isBackwardDeadline ? 'Termen limită' : 'Start'}: {formatDate(deadline.startDate)} + {' → '} + {def?.isBackwardDeadline ? 'Depunere până la' : 'Termen'}: {formatDate(deadline.dueDate)} + {def?.dayType === 'working' && (zile lucrătoare)} +
+
+
+ {deadline.resolution === 'pending' && ( + + )} + +
+
+ ); +} + +function formatDate(iso: string): string { + try { + return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }); + } catch { + return iso; + } +} diff --git a/src/modules/registratura/components/deadline-dashboard.tsx b/src/modules/registratura/components/deadline-dashboard.tsx new file mode 100644 index 0000000..4243fbc --- /dev/null +++ b/src/modules/registratura/components/deadline-dashboard.tsx @@ -0,0 +1,160 @@ +'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'; + +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; +} + +const RESOLUTION_LABELS: Record = { + pending: 'În așteptare', + completed: 'Finalizat', + 'aprobat-tacit': 'Aprobat tacit', + respins: 'Respins', + anulat: 'Anulat', +}; + +export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDeadline }: DeadlineDashboardProps) { + const { filters, updateFilter } = useDeadlineFilters(); + const [resolvingEntry, setResolvingEntry] = useState(null); + const [resolvingDeadline, setResolvingDeadline] = useState(null); + + const stats = useMemo(() => aggregateDeadlines(entries), [entries]); + + const filteredRows = useMemo(() => { + return stats.all.filter((row) => { + if (filters.category !== 'all') { + const def = getDeadlineType(row.deadline.typeId); + if (def && def.category !== filters.category) return false; + } + if (filters.resolution !== 'all') { + // Map tacit display status to actual resolution filter + 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; + } + return true; + }); + }, [stats.all, filters]); + + const handleResolveClick = (entryId: string, deadline: TrackedDeadline) => { + setResolvingEntry(entryId); + setResolvingDeadline(deadline); + }; + + const handleResolve = (resolution: DeadlineResolution, note: string, chainNext: boolean) => { + if (!resolvingEntry || !resolvingDeadline) return; + 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); + } + } + + setResolvingEntry(null); + setResolvingDeadline(null); + }; + + return ( +
+ {/* Stats */} +
+ + 0 ? 'destructive' : undefined} /> + 0 ? 'destructive' : undefined} /> + 0 ? 'blue' : undefined} /> +
+ + {/* Filters */} +
+
+ + +
+
+ + +
+ +
+ + {/* Table */} + + +

+ {filteredRows.length} din {stats.all.length} termene afișate +

+ + { + if (!open) { + setResolvingEntry(null); + setResolvingDeadline(null); + } + }} + onResolve={handleResolve} + /> +
+ ); +} + +function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' | 'blue' }) { + return ( + + +

{label}

+

0 ? 'text-destructive' : '' + }${variant === 'blue' && value > 0 ? 'text-blue-600' : ''}`}> + {value} +

+
+
+ ); +} diff --git a/src/modules/registratura/components/deadline-resolve-dialog.tsx b/src/modules/registratura/components/deadline-resolve-dialog.tsx new file mode 100644 index 0000000..a80770b --- /dev/null +++ b/src/modules/registratura/components/deadline-resolve-dialog.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { useState } from 'react'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/shared/components/ui/dialog'; +import { Button } from '@/shared/components/ui/button'; +import { Label } from '@/shared/components/ui/label'; +import { Textarea } from '@/shared/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; +import type { TrackedDeadline, DeadlineResolution } from '../types'; +import { getDeadlineType } from '../services/deadline-catalog'; + +interface DeadlineResolveDialogProps { + open: boolean; + deadline: TrackedDeadline | null; + onOpenChange: (open: boolean) => void; + onResolve: (resolution: DeadlineResolution, note: string, chainNext: boolean) => void; +} + +const RESOLUTION_OPTIONS: Array<{ value: DeadlineResolution; label: string }> = [ + { value: 'completed', label: 'Finalizat' }, + { value: 'aprobat-tacit', label: 'Aprobat tacit' }, + { value: 'respins', label: 'Respins' }, + { value: 'anulat', label: 'Anulat' }, +]; + +export function DeadlineResolveDialog({ open, deadline, onOpenChange, onResolve }: DeadlineResolveDialogProps) { + const [resolution, setResolution] = useState('completed'); + const [note, setNote] = useState(''); + + if (!deadline) return null; + + const def = getDeadlineType(deadline.typeId); + const hasChain = def?.chainNextTypeId && (resolution === 'completed' || resolution === 'aprobat-tacit'); + + const handleResolve = () => { + onResolve(resolution, note, !!hasChain); + setResolution('completed'); + setNote(''); + onOpenChange(false); + }; + + const handleClose = () => { + setResolution('completed'); + setNote(''); + onOpenChange(false); + }; + + return ( + { if (!o) handleClose(); }}> + + + Rezolvă — {def?.label ?? deadline.typeId} + + +
+
+ + +
+ +
+ +