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
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user