feat(registratura): detail sheet side panel + configurable column visibility
- New registry-entry-detail.tsx: full entry visualization in Sheet (side panel) - Status badges, document info, parties, dates, thread links - Attachment preview: images inline, NAS paths with IP fallback - Legal deadlines, external tracking, tags, notes sections - Action buttons: Editează, Închide, Șterge - Registry table rewrite: - 10 column defs with Romanian tooltip explanations on each header - Column visibility dropdown (Settings icon) with checkboxes - Default: Nr/Data/Dir/Subiect/Exped./Dest./Status (7/10) - Persisted in localStorage (registratura:visible-columns) - Row click opens detail sheet, actions reduced to Eye + Pencil - Docs updated: CLAUDE.md, ROADMAP.md (3.03c), SESSION-LOG.md
This commit is contained in:
@@ -101,7 +101,7 @@ legacy/ # Original HTML tools for reference
|
|||||||
| 1 | **Dashboard** | `/` | 0.1.0 | KPI cards (6), activity feed (last 20), module grid, external tools |
|
| 1 | **Dashboard** | `/` | 0.1.0 | KPI cards (6), activity feed (last 20), module grid, external tools |
|
||||||
| 2 | **Email Signature** | `/email-signature` | 0.1.0 | Multi-company branding, address toggle (BTG/US/SDT), live preview, zoom/copy/download |
|
| 2 | **Email Signature** | `/email-signature` | 0.1.0 | Multi-company branding, address toggle (BTG/US/SDT), live preview, zoom/copy/download |
|
||||||
| 3 | **Word XML Generator** | `/word-xml` | 0.1.0 | Category-based XML gen, simple/advanced mode, ZIP export |
|
| 3 | **Word XML Generator** | `/word-xml` | 0.1.0 | Category-based XML gen, simple/advanced mode, ZIP export |
|
||||||
| 4 | **Registratura** | `/registratura` | 0.3.0 | CRUD registry, dynamic doc types, bidirectional Address Book, threads, backdating, **legal deadline tracking**, recipient registration, document expiry, **NAS network path attachments** (A/O/P/T drives, hostname+IP fallback) |
|
| 4 | **Registratura** | `/registratura` | 0.3.0 | CRUD registry, dynamic doc types, bidirectional Address Book, threads, backdating, **legal deadline tracking**, recipient registration, document expiry, **NAS network path attachments** (A/O/P/T drives, hostname+IP fallback), **detail sheet side panel**, **configurable column visibility** |
|
||||||
| 5 | **Tag Manager** | `/tag-manager` | 0.2.0 | CRUD tags, category/scope/color, US/SDT seeds, mandatory categories, **ManicTime bidirectional sync** |
|
| 5 | **Tag Manager** | `/tag-manager` | 0.2.0 | CRUD tags, category/scope/color, US/SDT seeds, mandatory categories, **ManicTime bidirectional sync** |
|
||||||
| 6 | **IT Inventory** | `/it-inventory` | 0.2.0 | Dynamic equipment types, rented status (purple pulse), **42U rack visualization**, type/status/company filters |
|
| 6 | **IT Inventory** | `/it-inventory` | 0.2.0 | Dynamic equipment types, rented status (purple pulse), **42U rack visualization**, type/status/company filters |
|
||||||
| 7 | **Address Book** | `/address-book` | 0.1.0 | CRUD contacts, card grid, vCard export, Registratura reverse lookup, **dynamic types (creatable)** |
|
| 7 | **Address Book** | `/address-book` | 0.1.0 | CRUD contacts, card grid, vCard export, Registratura reverse lookup, **dynamic types (creatable)** |
|
||||||
|
|||||||
+12
@@ -304,6 +304,18 @@
|
|||||||
- ✅ **Drive letter → UNC** normalization automată (P:\ → \\newamun\Proiecte)
|
- ✅ **Drive letter → UNC** normalization automată (P:\ → \\newamun\Proiecte)
|
||||||
- ✅ **`shareLabelFor()`** helper returns human-readable share name
|
- ✅ **`shareLabelFor()`** helper returns human-readable share name
|
||||||
|
|
||||||
|
### 3.03c ✅ `[STANDARD]` Registratura — Detail Sheet + Column Manager (2026-02-28)
|
||||||
|
|
||||||
|
**Implementat:**
|
||||||
|
|
||||||
|
- ✅ **Entry Detail Sheet**: panou lateral (Sheet) cu vizualizare completă a înregistrării — status badges, date, părți, thread-uri, atașamente cu preview inline, termene legale, etichete, note
|
||||||
|
- ✅ **Attachment Preview**: imagini afișate inline, fișiere cu download, NAS paths cu UNC complet + IP fallback
|
||||||
|
- ✅ **Column Visibility Manager**: dropdown cu checkbox-uri pentru 10 coloane, persistat în localStorage
|
||||||
|
- ✅ **Default columns**: Nr., Data, Dir., Subiect, Exped., Dest., Status (7/10 vizibile implicit)
|
||||||
|
- ✅ **Tooltip naming convention**: fiecare header de coloană are tooltip cu explicație completă în română
|
||||||
|
- ✅ **Table UX cleanup**: click pe rând deschide detail sheet, acțiuni reduse la View + Edit, Close/Delete mutate în sheet
|
||||||
|
- ✅ **Row click navigation**: cursor-pointer pe rânduri, Eye icon + Pencil icon în acțiuni
|
||||||
|
|
||||||
### 3.04 ✅ `[ARCHITECTURE]` Autentificare & Identitate (2026-02-27)
|
### 3.04 ✅ `[ARCHITECTURE]` Autentificare & Identitate (2026-02-27)
|
||||||
|
|
||||||
**Cerințe noi:**
|
**Cerințe noi:**
|
||||||
|
|||||||
+27
-2
@@ -20,14 +20,39 @@ Continuation of QA improvements. NAS path enhancement: all 4 drives + DNS→IP f
|
|||||||
- Badge now shows share label (Proiecte/Arhiva/Organizare/Transfer) instead of generic "NAS"
|
- Badge now shows share label (Proiecte/Arhiva/Organizare/Transfer) instead of generic "NAS"
|
||||||
- Validation hint updated to show all 4 drive letters
|
- Validation hint updated to show all 4 drive letters
|
||||||
|
|
||||||
|
- **Registratura — Entry Detail Sheet (Side Panel):**
|
||||||
|
- New `registry-entry-detail.tsx` component (~500 lines)
|
||||||
|
- Side panel (Sheet) slides in from the right on Eye icon click or row click
|
||||||
|
- Full entry visualization: status badges, document info, parties, dates, thread links, legal deadlines, attachments with inline image preview, NAS path links with IP fallback, external tracking, tags, notes
|
||||||
|
- Action buttons inside sheet: Edită, Închide, Șterge
|
||||||
|
- Attachment preview: images display inline, files show download button, NAS paths show full UNC + short display + copy + IP fallback
|
||||||
|
|
||||||
|
- **Registratura — Column Visibility Manager:**
|
||||||
|
- Configurable columns via Settings dropdown in table header
|
||||||
|
- 10 columns defined with Romanian tooltips explaining each abbreviation
|
||||||
|
- Default visible: Nr., Data, Dir., Subiect, Exped., Dest., Status (7/10)
|
||||||
|
- Hidden by default: Tip, Resp., Termen (can be toggled on)
|
||||||
|
- Persisted in localStorage per user (`registratura:visible-columns`)
|
||||||
|
- Reset button restores defaults
|
||||||
|
|
||||||
|
- **Registratura — Table UX Cleanup:**
|
||||||
|
- Row click opens detail sheet (cursor-pointer)
|
||||||
|
- Actions reduced from 3 buttons (close/edit/delete) to 2 (view/edit)
|
||||||
|
- Close and Delete moved into detail sheet
|
||||||
|
- Column headers have tooltips explaining naming convention
|
||||||
|
- Attachment/thread/deadline indicators compacted in Subject column
|
||||||
|
|
||||||
- **Documentation updated:**
|
- **Documentation updated:**
|
||||||
- CLAUDE.md: Registratura v0.3.0, NAS drives in module table, NAS row in integrations table
|
- CLAUDE.md: Registratura v0.3.0 updated description
|
||||||
- ROADMAP.md: Registratura version bump, new task 3.03b with full feature list
|
- ROADMAP.md: New task 3.03c with detail sheet + column manager features
|
||||||
- SESSION-LOG.md: This session entry
|
- SESSION-LOG.md: This session entry
|
||||||
|
|
||||||
### Files Changed
|
### Files Changed
|
||||||
|
|
||||||
|
- **New:** `src/modules/registratura/components/registry-entry-detail.tsx`
|
||||||
- **Modified:** `src/config/nas-paths.ts` (4 drives, IP fallback helpers, shareLabelFor)
|
- **Modified:** `src/config/nas-paths.ts` (4 drives, IP fallback helpers, shareLabelFor)
|
||||||
|
- **Modified:** `src/modules/registratura/components/registry-table.tsx` (column visibility, tooltips, view button, row click)
|
||||||
|
- **Modified:** `src/modules/registratura/components/registratura-module.tsx` (detail sheet integration, handleView)
|
||||||
- **Modified:** `src/modules/registratura/components/registry-entry-form.tsx` (IP fallback link, share badge, validation hints)
|
- **Modified:** `src/modules/registratura/components/registry-entry-form.tsx` (IP fallback link, share badge, validation hints)
|
||||||
- **Modified:** `CLAUDE.md`, `ROADMAP.md`, `SESSION-LOG.md`
|
- **Modified:** `CLAUDE.md`, `ROADMAP.md`, `SESSION-LOG.md`
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { v4 as uuid } from "uuid";
|
|||||||
import { RegistryFilters } from "./registry-filters";
|
import { RegistryFilters } from "./registry-filters";
|
||||||
import { RegistryTable } from "./registry-table";
|
import { RegistryTable } from "./registry-table";
|
||||||
import { RegistryEntryForm } from "./registry-entry-form";
|
import { RegistryEntryForm } from "./registry-entry-form";
|
||||||
|
import { RegistryEntryDetail } from "./registry-entry-detail";
|
||||||
import { DeadlineDashboard } from "./deadline-dashboard";
|
import { DeadlineDashboard } from "./deadline-dashboard";
|
||||||
import { ThreadExplorer } from "./thread-explorer";
|
import { ThreadExplorer } from "./thread-explorer";
|
||||||
import { CloseGuardDialog } from "./close-guard-dialog";
|
import { CloseGuardDialog } from "./close-guard-dialog";
|
||||||
@@ -62,6 +63,7 @@ export function RegistraturaModule() {
|
|||||||
|
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
||||||
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
|
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
|
||||||
|
const [viewingEntry, setViewingEntry] = useState<RegistryEntry | null>(null);
|
||||||
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);
|
||||||
|
|
||||||
@@ -135,9 +137,15 @@ export function RegistraturaModule() {
|
|||||||
// Load full entry with attachment data (list mode strips base64)
|
// Load full entry with attachment data (list mode strips base64)
|
||||||
const full = await loadFullEntry(entry.id);
|
const full = await loadFullEntry(entry.id);
|
||||||
setEditingEntry(full ?? entry);
|
setEditingEntry(full ?? entry);
|
||||||
|
setViewingEntry(null); // Close detail sheet if open
|
||||||
setViewMode("edit");
|
setViewMode("edit");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleView = async (entry: RegistryEntry) => {
|
||||||
|
const full = await loadFullEntry(entry.id);
|
||||||
|
setViewingEntry(full ?? entry);
|
||||||
|
};
|
||||||
|
|
||||||
const handleNavigateEntry = async (entry: RegistryEntry) => {
|
const handleNavigateEntry = async (entry: RegistryEntry) => {
|
||||||
const full = await loadFullEntry(entry.id);
|
const full = await loadFullEntry(entry.id);
|
||||||
setEditingEntry(full ?? entry);
|
setEditingEntry(full ?? entry);
|
||||||
@@ -305,6 +313,7 @@ export function RegistraturaModule() {
|
|||||||
<RegistryTable
|
<RegistryTable
|
||||||
entries={entries}
|
entries={entries}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
onView={handleView}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onClose={handleCloseRequest}
|
onClose={handleCloseRequest}
|
||||||
@@ -359,6 +368,19 @@ export function RegistraturaModule() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Entry detail side panel */}
|
||||||
|
<RegistryEntryDetail
|
||||||
|
entry={viewingEntry}
|
||||||
|
open={viewingEntry !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setViewingEntry(null);
|
||||||
|
}}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onClose={handleCloseRequest}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
allEntries={allEntries}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Universal close dialog — reason, attachment, linked entry */}
|
{/* Universal close dialog — reason, attachment, linked entry */}
|
||||||
{closingEntry && (
|
{closingEntry && (
|
||||||
<CloseGuardDialog
|
<CloseGuardDialog
|
||||||
|
|||||||
@@ -0,0 +1,727 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownToLine,
|
||||||
|
ArrowUpFromLine,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle2,
|
||||||
|
Clock,
|
||||||
|
Copy,
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
FolderOpen,
|
||||||
|
GitBranch,
|
||||||
|
HardDrive,
|
||||||
|
Link2,
|
||||||
|
Paperclip,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
User,
|
||||||
|
X,
|
||||||
|
Image as ImageIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
|
import { ScrollArea } from "@/shared/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
} from "@/shared/components/ui/sheet";
|
||||||
|
import type { RegistryEntry } from "../types";
|
||||||
|
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
||||||
|
import { getOverdueDays } from "../services/registry-service";
|
||||||
|
import {
|
||||||
|
toFileUrl,
|
||||||
|
toFileUrlByIp,
|
||||||
|
shortDisplayPath,
|
||||||
|
shareLabelFor,
|
||||||
|
} from "@/config/nas-paths";
|
||||||
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface RegistryEntryDetailProps {
|
||||||
|
entry: RegistryEntry | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onEdit: (entry: RegistryEntry) => void;
|
||||||
|
onClose: (id: string) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
allEntries: RegistryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIRECTION_CONFIG = {
|
||||||
|
intrat: {
|
||||||
|
label: "Intrat",
|
||||||
|
icon: ArrowDownToLine,
|
||||||
|
class: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
|
||||||
|
},
|
||||||
|
iesit: {
|
||||||
|
label: "Ieșit",
|
||||||
|
icon: ArrowUpFromLine,
|
||||||
|
class:
|
||||||
|
"bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const STATUS_CONFIG = {
|
||||||
|
deschis: {
|
||||||
|
label: "Deschis",
|
||||||
|
class:
|
||||||
|
"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300",
|
||||||
|
},
|
||||||
|
inchis: {
|
||||||
|
label: "Închis",
|
||||||
|
class: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const RESOLUTION_LABELS: Record<string, string> = {
|
||||||
|
finalizat: "Finalizat",
|
||||||
|
"aprobat-tacit": "Aprobat tacit",
|
||||||
|
respins: "Respins",
|
||||||
|
retras: "Retras",
|
||||||
|
altele: "Altele",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEADLINE_RES_LABELS: Record<string, string> = {
|
||||||
|
pending: "În așteptare",
|
||||||
|
completed: "Finalizat",
|
||||||
|
"aprobat-tacit": "Aprobat tacit",
|
||||||
|
respins: "Respins",
|
||||||
|
anulat: "Anulat",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDocTypeLabel(type: string): string {
|
||||||
|
const label = DEFAULT_DOC_TYPE_LABELS[type];
|
||||||
|
if (label) return label;
|
||||||
|
return type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegistryEntryDetail({
|
||||||
|
entry,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onEdit,
|
||||||
|
onClose,
|
||||||
|
onDelete,
|
||||||
|
allEntries,
|
||||||
|
}: RegistryEntryDetailProps) {
|
||||||
|
const [previewAttachment, setPreviewAttachment] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
const dir = DIRECTION_CONFIG[entry.direction] ?? DIRECTION_CONFIG.intrat;
|
||||||
|
const DirIcon = dir.icon;
|
||||||
|
const status = STATUS_CONFIG[entry.status] ?? STATUS_CONFIG.deschis;
|
||||||
|
|
||||||
|
const overdueDays =
|
||||||
|
entry.status === "deschis" ? getOverdueDays(entry.deadline) : null;
|
||||||
|
const isOverdue = overdueDays !== null && overdueDays > 0;
|
||||||
|
|
||||||
|
const threadParent = entry.threadParentId
|
||||||
|
? allEntries.find((e) => e.id === entry.threadParentId)
|
||||||
|
: null;
|
||||||
|
const threadChildren = allEntries.filter(
|
||||||
|
(e) => e.threadParentId === entry.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent
|
||||||
|
side="right"
|
||||||
|
className="w-full sm:max-w-lg md:max-w-xl p-0 flex flex-col"
|
||||||
|
>
|
||||||
|
<SheetHeader className="px-6 pt-6 pb-0">
|
||||||
|
<div className="flex items-start justify-between gap-3 pr-8">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<SheetTitle className="text-lg font-bold font-mono">
|
||||||
|
{entry.number}
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription className="text-sm">
|
||||||
|
{entry.subject}
|
||||||
|
</SheetDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action bar */}
|
||||||
|
<div className="flex gap-2 mt-3">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
onEdit(entry);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="mr-1.5 h-3.5 w-3.5" /> Editează
|
||||||
|
</Button>
|
||||||
|
{entry.status === "deschis" && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-green-600 border-green-300 hover:bg-green-50 dark:border-green-700 dark:hover:bg-green-950/30"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
onClose(entry.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" /> Închide
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-destructive ml-auto"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
onDelete(entry.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-1.5 h-3.5 w-3.5" /> Șterge
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<Separator className="mt-4" />
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1 px-6">
|
||||||
|
<div className="space-y-5 py-4">
|
||||||
|
{/* ── Status row ── */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge className={cn("text-xs px-2.5 py-0.5", dir.class)}>
|
||||||
|
<DirIcon className="mr-1 h-3 w-3" />
|
||||||
|
{dir.label}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={cn("text-xs px-2.5 py-0.5", status.class)}>
|
||||||
|
{status.label}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{getDocTypeLabel(entry.documentType)}
|
||||||
|
</Badge>
|
||||||
|
{entry.company && (
|
||||||
|
<Badge variant="outline" className="text-xs uppercase">
|
||||||
|
{entry.company}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Closure info ── */}
|
||||||
|
{entry.closureInfo && (
|
||||||
|
<DetailSection title="Închidere">
|
||||||
|
<div className="rounded-md border border-muted p-3 space-y-1 text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">Rezoluție:</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{RESOLUTION_LABELS[entry.closureInfo.resolution] ??
|
||||||
|
entry.closureInfo.resolution}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{entry.closureInfo.reason && (
|
||||||
|
<p>
|
||||||
|
<span className="text-muted-foreground">Motiv:</span>{" "}
|
||||||
|
{entry.closureInfo.reason}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{entry.closureInfo.closedBy && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Închis de {entry.closureInfo.closedBy} la{" "}
|
||||||
|
{formatDateTime(entry.closureInfo.closedAt)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DetailSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Date ── */}
|
||||||
|
<DetailSection title="Date">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<DetailField
|
||||||
|
label="Data document"
|
||||||
|
value={formatDate(entry.date)}
|
||||||
|
/>
|
||||||
|
{entry.registrationDate &&
|
||||||
|
entry.registrationDate !== entry.date && (
|
||||||
|
<DetailField
|
||||||
|
label="Data înregistrare"
|
||||||
|
value={formatDate(entry.registrationDate)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{entry.deadline && (
|
||||||
|
<DetailField
|
||||||
|
label="Termen limită"
|
||||||
|
value={formatDate(entry.deadline)}
|
||||||
|
className={cn(isOverdue && "text-destructive font-medium")}
|
||||||
|
extra={
|
||||||
|
overdueDays !== null && overdueDays > 0
|
||||||
|
? `(${overdueDays} zile depășit)`
|
||||||
|
: overdueDays !== null && overdueDays < 0
|
||||||
|
? `(mai sunt ${Math.abs(overdueDays)} zile)`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{entry.expiryDate && (
|
||||||
|
<DetailField
|
||||||
|
label="Valabilitate"
|
||||||
|
value={formatDate(entry.expiryDate)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DetailField
|
||||||
|
label="Creat"
|
||||||
|
value={formatDateTime(entry.createdAt)}
|
||||||
|
/>
|
||||||
|
<DetailField
|
||||||
|
label="Modificat"
|
||||||
|
value={formatDateTime(entry.updatedAt)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DetailSection>
|
||||||
|
|
||||||
|
{/* ── Parties ── */}
|
||||||
|
<DetailSection title="Părți">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{entry.sender && (
|
||||||
|
<DetailField label="Expeditor" value={entry.sender} />
|
||||||
|
)}
|
||||||
|
{entry.recipient && (
|
||||||
|
<DetailField label="Destinatar" value={entry.recipient} />
|
||||||
|
)}
|
||||||
|
{entry.assignee && (
|
||||||
|
<DetailField
|
||||||
|
label="Responsabil"
|
||||||
|
value={entry.assignee}
|
||||||
|
icon={<User className="h-3 w-3 text-muted-foreground" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(entry.recipientRegNumber || entry.recipientRegDate) && (
|
||||||
|
<div className="mt-2 rounded border border-dashed p-2 text-xs text-muted-foreground">
|
||||||
|
{entry.recipientRegNumber && (
|
||||||
|
<span>Nr. destinatar: {entry.recipientRegNumber}</span>
|
||||||
|
)}
|
||||||
|
{entry.recipientRegDate && (
|
||||||
|
<span className="ml-3">
|
||||||
|
Data: {formatDate(entry.recipientRegDate)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DetailSection>
|
||||||
|
|
||||||
|
{/* ── Thread links ── */}
|
||||||
|
{(threadParent ||
|
||||||
|
threadChildren.length > 0 ||
|
||||||
|
(entry.linkedEntryIds ?? []).length > 0) && (
|
||||||
|
<DetailSection title="Legături">
|
||||||
|
{threadParent && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<GitBranch className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Răspuns la:</span>
|
||||||
|
<span className="font-mono text-xs">
|
||||||
|
{threadParent.number}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-muted-foreground">
|
||||||
|
— {threadParent.subject}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{threadChildren.length > 0 && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Răspunsuri ({threadChildren.length}):
|
||||||
|
</p>
|
||||||
|
{threadChildren.map((child) => (
|
||||||
|
<div
|
||||||
|
key={child.id}
|
||||||
|
className="flex items-center gap-2 text-xs ml-4"
|
||||||
|
>
|
||||||
|
<span className="font-mono">{child.number}</span>
|
||||||
|
<span className="truncate text-muted-foreground">
|
||||||
|
{child.subject}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(entry.linkedEntryIds ?? []).length > 0 && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<Link2 className="h-3 w-3" />
|
||||||
|
{entry.linkedEntryIds.length} înregistrăr
|
||||||
|
{entry.linkedEntryIds.length === 1 ? "e" : "i"} legat
|
||||||
|
{entry.linkedEntryIds.length === 1 ? "ă" : "e"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DetailSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Attachments ── */}
|
||||||
|
{entry.attachments.length > 0 && (
|
||||||
|
<DetailSection title={`Atașamente (${entry.attachments.length})`}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{entry.attachments.map((att) =>
|
||||||
|
att.networkPath ? (
|
||||||
|
<div
|
||||||
|
key={att.id}
|
||||||
|
className="flex items-center gap-2 rounded-md border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20 px-3 py-2 group"
|
||||||
|
>
|
||||||
|
<HardDrive className="h-4 w-4 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<a
|
||||||
|
href={toFileUrl(att.networkPath)}
|
||||||
|
className="text-sm text-blue-700 dark:text-blue-300 hover:underline font-mono"
|
||||||
|
title={att.networkPath}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open(
|
||||||
|
toFileUrl(att.networkPath!),
|
||||||
|
"_blank",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FolderOpen className="mr-1 inline h-3 w-3" />
|
||||||
|
{shortDisplayPath(att.networkPath)}
|
||||||
|
</a>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||||
|
{att.networkPath}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{shareLabelFor(att.networkPath) ?? "NAS"}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={() =>
|
||||||
|
navigator.clipboard.writeText(att.networkPath!)
|
||||||
|
}
|
||||||
|
title="Copiază calea"
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity text-blue-500"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
toFileUrlByIp(att.networkPath!),
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title="Deschide prin IP (fallback)"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={att.id}
|
||||||
|
className="flex items-center gap-2 rounded-md border px-3 py-2 group"
|
||||||
|
>
|
||||||
|
{att.type.startsWith("image/") ? (
|
||||||
|
<ImageIcon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
|
) : (
|
||||||
|
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm truncate">{att.name}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{(att.size / 1024).toFixed(0)} KB •{" "}
|
||||||
|
{att.type.split("/")[1]?.toUpperCase() ?? att.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
{/* Preview for images */}
|
||||||
|
{att.type.startsWith("image/") &&
|
||||||
|
att.data &&
|
||||||
|
att.data !== "" &&
|
||||||
|
att.data !== "__network__" && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => setPreviewAttachment(att.id)}
|
||||||
|
title="Previzualizare"
|
||||||
|
>
|
||||||
|
<ImageIcon className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* Download for files with data */}
|
||||||
|
{att.data &&
|
||||||
|
att.data !== "" &&
|
||||||
|
att.data !== "__network__" && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = att.data;
|
||||||
|
a.download = att.name;
|
||||||
|
a.click();
|
||||||
|
}}
|
||||||
|
title="Descarcă"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
<Paperclip className="mr-0.5 h-2.5 w-2.5" />
|
||||||
|
Fișier
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image preview modal */}
|
||||||
|
{previewAttachment && (
|
||||||
|
<div className="mt-3 rounded-md border p-2 bg-muted/30">
|
||||||
|
{(() => {
|
||||||
|
const att = entry.attachments.find(
|
||||||
|
(a) => a.id === previewAttachment,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!att ||
|
||||||
|
!att.type.startsWith("image/") ||
|
||||||
|
!att.data ||
|
||||||
|
att.data === "" ||
|
||||||
|
att.data === "__network__"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Previzualizare indisponibilă (fișierul nu conține
|
||||||
|
date inline).
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-medium">{att.name}</p>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5"
|
||||||
|
onClick={() => setPreviewAttachment(null)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={att.data}
|
||||||
|
alt={att.name}
|
||||||
|
className="max-w-full max-h-64 rounded border object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DetailSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Legal deadlines ── */}
|
||||||
|
{(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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── External tracking ── */}
|
||||||
|
{(entry.externalStatusUrl || entry.externalTrackingId) && (
|
||||||
|
<DetailSection title="Urmărire externă">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{entry.externalTrackingId && (
|
||||||
|
<DetailField
|
||||||
|
label="ID extern"
|
||||||
|
value={entry.externalTrackingId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{entry.externalStatusUrl && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground mb-0.5">
|
||||||
|
URL verificare
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={entry.externalStatusUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-blue-600 dark:text-blue-400 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{entry.externalStatusUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DetailSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Tags ── */}
|
||||||
|
{entry.tags.length > 0 && (
|
||||||
|
<DetailSection title="Etichete">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{entry.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DetailSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Notes ── */}
|
||||||
|
{entry.notes && (
|
||||||
|
<DetailSection title="Note">
|
||||||
|
<p className="text-sm whitespace-pre-wrap text-muted-foreground">
|
||||||
|
{entry.notes}
|
||||||
|
</p>
|
||||||
|
</DetailSection>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sub-components ──
|
||||||
|
|
||||||
|
function DetailSection({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
extra,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
className?: string;
|
||||||
|
extra?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground mb-0.5">{label}</p>
|
||||||
|
<p className={cn("text-sm", className)}>
|
||||||
|
{icon && <span className="mr-1 inline-flex align-middle">{icon}</span>}
|
||||||
|
{value}
|
||||||
|
{extra && (
|
||||||
|
<span className="ml-1 text-[10px] text-muted-foreground">
|
||||||
|
{extra}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
|
Eye,
|
||||||
Pencil,
|
Pencil,
|
||||||
Trash2,
|
|
||||||
CheckCircle2,
|
|
||||||
Link2,
|
Link2,
|
||||||
Clock,
|
Clock,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
User,
|
User,
|
||||||
|
Settings2,
|
||||||
|
Paperclip,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/shared/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/shared/components/ui/dropdown-menu";
|
||||||
import type { RegistryEntry } from "../types";
|
import type { RegistryEntry } from "../types";
|
||||||
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
||||||
import { getOverdueDays } from "../services/registry-service";
|
import { getOverdueDays } from "../services/registry-service";
|
||||||
@@ -19,21 +35,117 @@ import { cn } from "@/shared/lib/utils";
|
|||||||
interface RegistryTableProps {
|
interface RegistryTableProps {
|
||||||
entries: RegistryEntry[];
|
entries: RegistryEntry[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
onView: (entry: RegistryEntry) => void;
|
||||||
onEdit: (entry: RegistryEntry) => void;
|
onEdit: (entry: RegistryEntry) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
onClose: (id: string) => void;
|
onClose: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Column definitions ──
|
||||||
|
|
||||||
|
export type ColumnId =
|
||||||
|
| "number"
|
||||||
|
| "date"
|
||||||
|
| "direction"
|
||||||
|
| "type"
|
||||||
|
| "subject"
|
||||||
|
| "sender"
|
||||||
|
| "recipient"
|
||||||
|
| "assignee"
|
||||||
|
| "deadline"
|
||||||
|
| "status";
|
||||||
|
|
||||||
|
interface ColumnDef {
|
||||||
|
id: ColumnId;
|
||||||
|
/** Short header label */
|
||||||
|
label: string;
|
||||||
|
/** Full tooltip explanation */
|
||||||
|
tooltip: string;
|
||||||
|
/** Whether visible by default */
|
||||||
|
defaultVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLUMNS: ColumnDef[] = [
|
||||||
|
{
|
||||||
|
id: "number",
|
||||||
|
label: "Nr.",
|
||||||
|
tooltip: "Număr de înregistrare (format: PREFIX-NNNN/AN)",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "date",
|
||||||
|
label: "Data",
|
||||||
|
tooltip: "Data documentului (nu data înregistrării în sistem)",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "direction",
|
||||||
|
label: "Dir.",
|
||||||
|
tooltip:
|
||||||
|
"Direcție: Intrat = primit de la terți, Ieșit = trimis către terți",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "type",
|
||||||
|
label: "Tip",
|
||||||
|
tooltip: "Tipul documentului (contract, cerere, aviz etc.)",
|
||||||
|
defaultVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "subject",
|
||||||
|
label: "Subiect",
|
||||||
|
tooltip: "Subiectul sau descrierea pe scurt a documentului",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sender",
|
||||||
|
label: "Exped.",
|
||||||
|
tooltip: "Expeditor — persoana sau instituția care a trimis documentul",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "recipient",
|
||||||
|
label: "Dest.",
|
||||||
|
tooltip:
|
||||||
|
"Destinatar — persoana sau instituția căreia i se adresează documentul",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "assignee",
|
||||||
|
label: "Resp.",
|
||||||
|
tooltip:
|
||||||
|
"Responsabil intern — persoana din echipă alocată pe acest document",
|
||||||
|
defaultVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deadline",
|
||||||
|
label: "Termen",
|
||||||
|
tooltip:
|
||||||
|
"Termen limită intern (nu termen legal — acela apare în tab-ul Termene)",
|
||||||
|
defaultVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
label: "Status",
|
||||||
|
tooltip: "Deschis = în lucru, Închis = finalizat/arhivat",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_VISIBLE = new Set(
|
||||||
|
COLUMNS.filter((c) => c.defaultVisible).map((c) => c.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const STORAGE_KEY = "registratura:visible-columns";
|
||||||
|
|
||||||
const DIRECTION_LABELS: Record<string, string> = {
|
const DIRECTION_LABELS: Record<string, string> = {
|
||||||
intrat: "Intrat",
|
intrat: "Intrat",
|
||||||
iesit: "Ieșit",
|
iesit: "Ieșit",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Resolve doc type label from defaults or capitalize custom type */
|
|
||||||
function getDocTypeLabel(type: string): string {
|
function getDocTypeLabel(type: string): string {
|
||||||
const label = DEFAULT_DOC_TYPE_LABELS[type];
|
const label = DEFAULT_DOC_TYPE_LABELS[type];
|
||||||
if (label) return label;
|
if (label) return label;
|
||||||
// For custom types, capitalize first letter
|
|
||||||
return type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
return type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +154,58 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
inchis: "Închis",
|
inchis: "Închis",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function loadVisibleColumns(): Set<ColumnId> {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const arr = JSON.parse(raw) as ColumnId[];
|
||||||
|
if (Array.isArray(arr) && arr.length > 0) return new Set(arr);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return new Set(DEFAULT_VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
export function RegistryTable({
|
export function RegistryTable({
|
||||||
entries,
|
entries,
|
||||||
loading,
|
loading,
|
||||||
|
onView,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClose,
|
onClose,
|
||||||
}: RegistryTableProps) {
|
}: RegistryTableProps) {
|
||||||
|
const [visibleCols, setVisibleCols] = useState<Set<ColumnId>>(
|
||||||
|
() => DEFAULT_VISIBLE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load from localStorage on mount (client-only)
|
||||||
|
useEffect(() => {
|
||||||
|
setVisibleCols(loadVisibleColumns());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleColumn = useCallback((id: ColumnId) => {
|
||||||
|
setVisibleCols((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
// Don't allow hiding all columns
|
||||||
|
if (next.size > 2) next.delete(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify([...next]));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetColumns = useCallback(() => {
|
||||||
|
const def = new Set(DEFAULT_VISIBLE);
|
||||||
|
setVisibleCols(def);
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify([...def]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const visibleColumns = COLUMNS.filter((c) => visibleCols.has(c.id));
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
@@ -66,170 +223,250 @@ export function RegistryTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto rounded-lg border">
|
<div className="space-y-2">
|
||||||
<table className="w-full text-sm">
|
{/* Column toggle button */}
|
||||||
<thead>
|
<div className="flex justify-end">
|
||||||
<tr className="border-b bg-muted/40">
|
<DropdownMenu>
|
||||||
<th className="px-3 py-2 text-left font-medium">Nr.</th>
|
<DropdownMenuTrigger asChild>
|
||||||
<th className="px-3 py-2 text-left font-medium">Data doc.</th>
|
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1">
|
||||||
<th className="px-3 py-2 text-left font-medium">Dir.</th>
|
<Settings2 className="h-3.5 w-3.5" />
|
||||||
<th className="px-3 py-2 text-left font-medium">Tip</th>
|
Coloane ({visibleCols.size}/{COLUMNS.length})
|
||||||
<th className="px-3 py-2 text-left font-medium">Subiect</th>
|
</Button>
|
||||||
<th className="px-3 py-2 text-left font-medium">Expeditor</th>
|
</DropdownMenuTrigger>
|
||||||
<th className="px-3 py-2 text-left font-medium">Destinatar</th>
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
<th className="px-3 py-2 text-left font-medium">Resp.</th>
|
<DropdownMenuLabel className="text-xs">
|
||||||
<th className="px-3 py-2 text-left font-medium">Termen</th>
|
Coloane vizibile
|
||||||
<th className="px-3 py-2 text-left font-medium">Status</th>
|
</DropdownMenuLabel>
|
||||||
<th className="px-3 py-2 text-right font-medium">Acțiuni</th>
|
<DropdownMenuSeparator />
|
||||||
</tr>
|
{COLUMNS.map((col) => (
|
||||||
</thead>
|
<DropdownMenuCheckboxItem
|
||||||
<tbody>
|
key={col.id}
|
||||||
{entries.map((entry) => {
|
checked={visibleCols.has(col.id)}
|
||||||
const overdueDays =
|
onCheckedChange={() => toggleColumn(col.id)}
|
||||||
entry.status === "deschis" || !entry.status
|
onSelect={(e) => e.preventDefault()}
|
||||||
? getOverdueDays(entry.deadline)
|
|
||||||
: null;
|
|
||||||
const isOverdue = overdueDays !== null && overdueDays > 0;
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={entry.id}
|
|
||||||
className={cn(
|
|
||||||
"border-b transition-colors hover:bg-muted/20",
|
|
||||||
isOverdue && "bg-destructive/5",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">
|
<span className="text-xs">
|
||||||
{entry.number}
|
{col.label}{" "}
|
||||||
</td>
|
<span className="text-muted-foreground">— {col.tooltip}</span>
|
||||||
<td className="px-3 py-2 text-xs whitespace-nowrap">
|
</span>
|
||||||
{formatDate(entry.date)}
|
</DropdownMenuCheckboxItem>
|
||||||
{entry.registrationDate &&
|
))}
|
||||||
entry.registrationDate !== entry.date && (
|
<DropdownMenuSeparator />
|
||||||
<span
|
<DropdownMenuCheckboxItem
|
||||||
className="block text-[10px] text-muted-foreground"
|
checked={false}
|
||||||
title={`Înregistrat pe ${formatDate(entry.registrationDate)}`}
|
onCheckedChange={resetColumns}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-medium">Resetează la implicit</span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-x-auto rounded-lg border">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-muted/40">
|
||||||
|
{visibleColumns.map((col) => (
|
||||||
|
<TooltipProvider key={col.id}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<th className="px-3 py-2 text-left font-medium cursor-help text-xs">
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" className="max-w-xs">
|
||||||
|
<p>{col.tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
))}
|
||||||
|
{/* Actions column is always shown */}
|
||||||
|
<th className="px-3 py-2 text-right font-medium text-xs w-20">
|
||||||
|
Acț.
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{entries.map((entry) => {
|
||||||
|
const overdueDays =
|
||||||
|
entry.status === "deschis" || !entry.status
|
||||||
|
? getOverdueDays(entry.deadline)
|
||||||
|
: null;
|
||||||
|
const isOverdue = overdueDays !== null && overdueDays > 0;
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={entry.id}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/20 cursor-pointer",
|
||||||
|
isOverdue && "bg-destructive/5",
|
||||||
|
)}
|
||||||
|
onClick={() => onView(entry)}
|
||||||
|
>
|
||||||
|
{visibleCols.has("number") && (
|
||||||
|
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">
|
||||||
|
{entry.number}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{visibleCols.has("date") && (
|
||||||
|
<td className="px-3 py-2 text-xs whitespace-nowrap">
|
||||||
|
{formatDate(entry.date)}
|
||||||
|
{entry.registrationDate &&
|
||||||
|
entry.registrationDate !== entry.date && (
|
||||||
|
<span
|
||||||
|
className="block text-[10px] text-muted-foreground"
|
||||||
|
title={`Înregistrat pe ${formatDate(entry.registrationDate)}`}
|
||||||
|
>
|
||||||
|
(înr. {formatDate(entry.registrationDate)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{visibleCols.has("direction") && (
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
entry.direction === "intrat" ? "default" : "secondary"
|
||||||
|
}
|
||||||
|
className="text-xs"
|
||||||
>
|
>
|
||||||
(înr. {formatDate(entry.registrationDate)})
|
{DIRECTION_LABELS[entry.direction] ??
|
||||||
|
entry.direction ??
|
||||||
|
"—"}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{visibleCols.has("type") && (
|
||||||
|
<td className="px-3 py-2 text-xs">
|
||||||
|
{getDocTypeLabel(entry.documentType)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{visibleCols.has("subject") && (
|
||||||
|
<td className="px-3 py-2 max-w-[220px]">
|
||||||
|
<span className="truncate block">{entry.subject}</span>
|
||||||
|
{/* Inline indicators */}
|
||||||
|
<span className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||||
|
{entry.threadParentId && (
|
||||||
|
<GitBranch className="inline h-3 w-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{(entry.linkedEntryIds ?? []).length > 0 && (
|
||||||
|
<Link2 className="inline h-3 w-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{(entry.attachments ?? []).length > 0 && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
<Paperclip className="mr-0.5 inline h-2.5 w-2.5" />
|
||||||
|
{entry.attachments.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{(entry.trackedDeadlines ?? []).length > 0 && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
<Clock className="mr-0.5 inline h-2.5 w-2.5" />
|
||||||
|
{(entry.trackedDeadlines ?? []).length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
</td>
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2">
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
entry.direction === "intrat" ? "default" : "secondary"
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{DIRECTION_LABELS[entry.direction] ??
|
|
||||||
entry.direction ??
|
|
||||||
"—"}
|
|
||||||
</Badge>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 text-xs">
|
|
||||||
{getDocTypeLabel(entry.documentType)}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 max-w-[200px] truncate">
|
|
||||||
{entry.subject}
|
|
||||||
{entry.threadParentId && (
|
|
||||||
<GitBranch className="ml-1 inline h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
)}
|
||||||
{(entry.linkedEntryIds ?? []).length > 0 && (
|
{visibleCols.has("sender") && (
|
||||||
<Link2 className="ml-1 inline h-3 w-3 text-muted-foreground" />
|
<td className="px-3 py-2 max-w-[130px] truncate text-xs">
|
||||||
)}
|
{entry.sender || (
|
||||||
{(entry.attachments ?? []).length > 0 && (
|
<span className="text-muted-foreground">—</span>
|
||||||
<Badge variant="outline" className="ml-1 text-[10px] px-1">
|
|
||||||
{entry.attachments.length} fișiere
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{(entry.trackedDeadlines ?? []).length > 0 && (
|
|
||||||
<Badge variant="outline" className="ml-1 text-[10px] px-1">
|
|
||||||
<Clock className="mr-0.5 inline h-2.5 w-2.5" />
|
|
||||||
{(entry.trackedDeadlines ?? []).length}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 max-w-[130px] truncate">
|
|
||||||
{entry.sender}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 max-w-[130px] truncate">
|
|
||||||
{entry.recipient}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 max-w-[100px] truncate text-xs">
|
|
||||||
{entry.assignee ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<User className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
||||||
{entry.assignee}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">—</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 text-xs whitespace-nowrap">
|
|
||||||
{entry.deadline ? (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
isOverdue && "font-medium text-destructive",
|
|
||||||
)}
|
)}
|
||||||
>
|
</td>
|
||||||
{formatDate(entry.deadline)}
|
)}
|
||||||
{overdueDays !== null && overdueDays > 0 && (
|
{visibleCols.has("recipient") && (
|
||||||
<span className="ml-1 text-[10px]">
|
<td className="px-3 py-2 max-w-[130px] truncate text-xs">
|
||||||
({overdueDays}z depășit)
|
{entry.recipient || (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{visibleCols.has("assignee") && (
|
||||||
|
<td className="px-3 py-2 max-w-[100px] truncate text-xs">
|
||||||
|
{entry.assignee ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<User className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||||
|
{entry.assignee}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
)}
|
)}
|
||||||
{overdueDays !== null && overdueDays < 0 && (
|
</td>
|
||||||
<span className="ml-1 text-[10px] text-muted-foreground">
|
|
||||||
({Math.abs(overdueDays)}z)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">—</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
{visibleCols.has("deadline") && (
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2 text-xs whitespace-nowrap">
|
||||||
<Badge
|
{entry.deadline ? (
|
||||||
variant={entry.status === "deschis" ? "default" : "outline"}
|
<span
|
||||||
>
|
className={cn(
|
||||||
{STATUS_LABELS[entry.status]}
|
isOverdue && "font-medium text-destructive",
|
||||||
</Badge>
|
)}
|
||||||
</td>
|
>
|
||||||
<td className="px-3 py-2 text-right">
|
{formatDate(entry.deadline)}
|
||||||
<div className="flex justify-end gap-1">
|
{overdueDays !== null && overdueDays > 0 && (
|
||||||
{entry.status === "deschis" && (
|
<span className="ml-1 text-[10px]">
|
||||||
|
({overdueDays}z)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{visibleCols.has("status") && (
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
entry.status === "deschis" ? "default" : "outline"
|
||||||
|
}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{STATUS_LABELS[entry.status]}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{/* Actions — always visible */}
|
||||||
|
<td className="px-3 py-2 text-right">
|
||||||
|
<div className="flex justify-end gap-0.5">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 text-green-600"
|
className="h-7 w-7"
|
||||||
onClick={() => onClose(entry.id)}
|
onClick={(e) => {
|
||||||
title="Închide"
|
e.stopPropagation();
|
||||||
|
onView(entry);
|
||||||
|
}}
|
||||||
|
title="Vizualizare detalii"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
<Eye className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="icon"
|
||||||
size="icon"
|
className="h-7 w-7"
|
||||||
className="h-7 w-7"
|
onClick={(e) => {
|
||||||
onClick={() => onEdit(entry)}
|
e.stopPropagation();
|
||||||
>
|
onEdit(entry);
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
}}
|
||||||
</Button>
|
title="Editează"
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
size="icon"
|
</Button>
|
||||||
className="h-7 w-7 text-destructive"
|
</div>
|
||||||
onClick={() => onDelete(entry.id)}
|
</td>
|
||||||
>
|
</tr>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
);
|
||||||
</Button>
|
})}
|
||||||
</div>
|
</tbody>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user