Files
ArchiTools/src/modules/registratura/components/thread-view.tsx
T
AI Assistant 2be0462e0d feat(registratura): 3.02 bidirectional integration, simplified status, threads
- Dynamic document types: string-based DocumentType synced with Tag Manager
  (new types auto-create tags under 'document-type' category)
- Added default types: 'Apel telefonic', 'Videoconferinta'
- Bidirectional Address Book: quick-create contacts from sender/recipient/
  assignee fields via QuickContactDialog popup
- Simplified status: Switch toggle replaces dropdown (default open)
- Responsabil (Assignee) field with contact autocomplete (ERP-ready)
- Entry threads: threadParentId links entries as replies, ThreadView shows
  parent/current/children tree with branching support
- Info tooltips on deadline, status, and assignee fields
- New Resp. column and thread icon in registry table
- All changes backward-compatible with existing data
2026-02-27 15:33:29 +02:00

152 lines
4.5 KiB
TypeScript

"use client";
import { CornerDownRight, GitBranch } from "lucide-react";
import { Badge } from "@/shared/components/ui/badge";
import type { RegistryEntry } from "../types";
import { cn } from "@/shared/lib/utils";
interface ThreadViewProps {
/** The current entry being viewed */
entry: RegistryEntry;
/** All entries in the registry (to resolve references) */
allEntries: RegistryEntry[];
/** Click on an entry to navigate to it */
onNavigate?: (entry: RegistryEntry) => void;
}
/**
* Shows thread relationships for a registry entry:
* - Parent entry (this is a reply to...)
* - Child entries (replies to this entry)
* Displays as an indented tree with direction badges.
*/
export function ThreadView({ entry, allEntries, onNavigate }: ThreadViewProps) {
// Find the parent entry (if this is a reply)
const parent = entry.threadParentId
? allEntries.find((e) => e.id === entry.threadParentId)
: null;
// Find child entries (replies to this entry)
const children = allEntries.filter((e) => e.threadParentId === entry.id);
// Find siblings (other replies to the same parent, excluding this one)
const siblings = entry.threadParentId
? allEntries.filter(
(e) => e.threadParentId === entry.threadParentId && e.id !== entry.id,
)
: [];
if (!parent && children.length === 0) return null;
return (
<div className="rounded-lg border bg-muted/20 p-3 space-y-2">
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<GitBranch className="h-3.5 w-3.5" />
Fir conversație
</div>
{/* Parent */}
{parent && (
<div className="space-y-1">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
Răspuns la:
</p>
<ThreadEntryChip entry={parent} onNavigate={onNavigate} />
{/* Siblings — other branches from same parent */}
{siblings.length > 0 && (
<div className="ml-4 space-y-1">
<p className="text-[10px] text-muted-foreground">
Alte ramuri ({siblings.length}):
</p>
{siblings.map((s) => (
<ThreadEntryChip
key={s.id}
entry={s}
onNavigate={onNavigate}
dimmed
/>
))}
</div>
)}
</div>
)}
{/* Current entry marker */}
<div className={cn("flex items-center gap-1.5", parent && "ml-4")}>
<CornerDownRight className="h-3 w-3 text-primary" />
<Badge variant="default" className="text-[10px]">
{entry.number}
</Badge>
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
{entry.subject}
</span>
</div>
{/* Children — replies to this entry */}
{children.length > 0 && (
<div className={cn("ml-8 space-y-1", !parent && "ml-4")}>
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
Răspunsuri ({children.length}):
</p>
{children.map((child) => (
<ThreadEntryChip
key={child.id}
entry={child}
onNavigate={onNavigate}
/>
))}
</div>
)}
</div>
);
}
function ThreadEntryChip({
entry,
onNavigate,
dimmed,
}: {
entry: RegistryEntry;
onNavigate?: (entry: RegistryEntry) => void;
dimmed?: boolean;
}) {
return (
<button
type="button"
onClick={() => onNavigate?.(entry)}
className={cn(
"flex items-center gap-1.5 rounded border px-2 py-1 text-left text-xs transition-colors hover:bg-accent w-full",
dimmed && "opacity-60",
)}
>
<Badge
variant={entry.direction === "intrat" ? "default" : "secondary"}
className="text-[9px] px-1 py-0 shrink-0"
>
{entry.direction === "intrat" ? "↓" : "↑"}
</Badge>
<span className="font-mono shrink-0">{entry.number}</span>
<span className="truncate text-muted-foreground">
{entry.subject.length > 40
? entry.subject.slice(0, 40) + "…"
: entry.subject}
</span>
<span className="ml-auto text-[10px] text-muted-foreground shrink-0">
{formatDate(entry.date)}
</span>
</button>
);
}
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso;
}
}