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,111 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { UserPlus } from "lucide-react";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
|
||||
interface QuickContactDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** Pre-filled name from the text the user typed */
|
||||
initialName: string;
|
||||
onConfirm: (data: { name: string; phone: string; email: string }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rapid popup for creating a new Address Book contact from Registratura.
|
||||
* Only requires Name; Phone and Email are optional.
|
||||
*/
|
||||
export function QuickContactDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialName,
|
||||
onConfirm,
|
||||
}: QuickContactDialogProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
// Reset when dialog opens with new name
|
||||
const handleOpenChange = (o: boolean) => {
|
||||
if (o) {
|
||||
setName(initialName);
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
onOpenChange(o);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
onConfirm({ name: name.trim(), phone: phone.trim(), email: email.trim() });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<UserPlus className="h-4 w-4" />
|
||||
Contact nou rapid
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div>
|
||||
<Label>Nume *</Label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-1"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>Telefon</Label>
|
||||
<Input
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Opțional"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Opțional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Anulează
|
||||
</Button>
|
||||
<Button type="submit" disabled={!name.trim()}>
|
||||
Creează contact
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +1,140 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/shared/components/ui/dialog';
|
||||
import { useRegistry } from '../hooks/use-registry';
|
||||
import { RegistryFilters } from './registry-filters';
|
||||
import { RegistryTable } from './registry-table';
|
||||
import { RegistryEntryForm } from './registry-entry-form';
|
||||
import { DeadlineDashboard } from './deadline-dashboard';
|
||||
import { getOverdueDays } from '../services/registry-service';
|
||||
import { aggregateDeadlines } from '../services/deadline-service';
|
||||
import type { RegistryEntry, DeadlineResolution } from '../types';
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/shared/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
import { useRegistry } from "../hooks/use-registry";
|
||||
import { useContacts } from "@/modules/address-book/hooks/use-contacts";
|
||||
import { useTags } from "@/core/tagging";
|
||||
import { RegistryFilters } from "./registry-filters";
|
||||
import { RegistryTable } from "./registry-table";
|
||||
import { RegistryEntryForm } from "./registry-entry-form";
|
||||
import { DeadlineDashboard } from "./deadline-dashboard";
|
||||
import { getOverdueDays } from "../services/registry-service";
|
||||
import { aggregateDeadlines } from "../services/deadline-service";
|
||||
import type { RegistryEntry, DeadlineResolution } from "../types";
|
||||
import type { AddressContact } from "@/modules/address-book/types";
|
||||
|
||||
type ViewMode = 'list' | 'add' | 'edit';
|
||||
type ViewMode = "list" | "add" | "edit";
|
||||
|
||||
export function RegistraturaModule() {
|
||||
const {
|
||||
entries, allEntries, loading, filters, updateFilter,
|
||||
addEntry, updateEntry, removeEntry, closeEntry,
|
||||
addDeadline, resolveDeadline, removeDeadline,
|
||||
entries,
|
||||
allEntries,
|
||||
loading,
|
||||
filters,
|
||||
updateFilter,
|
||||
addEntry,
|
||||
updateEntry,
|
||||
removeEntry,
|
||||
closeEntry,
|
||||
addDeadline,
|
||||
resolveDeadline,
|
||||
removeDeadline,
|
||||
} = useRegistry();
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const { addContact } = useContacts();
|
||||
const { createTag } = useTags("document-type");
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
||||
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
|
||||
const [closingId, setClosingId] = useState<string | null>(null);
|
||||
|
||||
const handleAdd = async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
|
||||
// ── Bidirectional Address Book integration ──
|
||||
const handleCreateContact = useCallback(
|
||||
async (data: {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}): Promise<AddressContact | undefined> => {
|
||||
try {
|
||||
const contact = await addContact({
|
||||
name: data.name,
|
||||
company: "",
|
||||
type: "collaborator",
|
||||
email: data.email,
|
||||
email2: "",
|
||||
phone: data.phone,
|
||||
phone2: "",
|
||||
address: "",
|
||||
department: "",
|
||||
role: "",
|
||||
website: "",
|
||||
projectIds: [],
|
||||
contactPersons: [],
|
||||
tags: [],
|
||||
notes: "Creat automat din Registratură",
|
||||
visibility: "all",
|
||||
});
|
||||
return contact;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[addContact],
|
||||
);
|
||||
|
||||
// ── Bidirectional Tag Manager integration ──
|
||||
const handleCreateDocType = useCallback(
|
||||
async (label: string) => {
|
||||
try {
|
||||
await createTag({
|
||||
label,
|
||||
category: "document-type",
|
||||
scope: "global",
|
||||
color: "#64748b",
|
||||
});
|
||||
} catch {
|
||||
// tag may already exist — ignore
|
||||
}
|
||||
},
|
||||
[createTag],
|
||||
);
|
||||
|
||||
const handleAdd = async (
|
||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||
) => {
|
||||
await addEntry(data);
|
||||
setViewMode('list');
|
||||
setViewMode("list");
|
||||
};
|
||||
|
||||
const handleEdit = (entry: RegistryEntry) => {
|
||||
setEditingEntry(entry);
|
||||
setViewMode('edit');
|
||||
setViewMode("edit");
|
||||
};
|
||||
|
||||
const handleUpdate = async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
|
||||
const handleNavigateEntry = (entry: RegistryEntry) => {
|
||||
setEditingEntry(entry);
|
||||
setViewMode("edit");
|
||||
};
|
||||
|
||||
const handleUpdate = async (
|
||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||
) => {
|
||||
if (!editingEntry) return;
|
||||
await updateEntry(editingEntry.id, data);
|
||||
setEditingEntry(null);
|
||||
setViewMode('list');
|
||||
setViewMode("list");
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
@@ -69,7 +158,7 @@ export function RegistraturaModule() {
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setViewMode('list');
|
||||
setViewMode("list");
|
||||
setEditingEntry(null);
|
||||
};
|
||||
|
||||
@@ -84,24 +173,34 @@ export function RegistraturaModule() {
|
||||
await resolveDeadline(entryId, deadlineId, resolution, note);
|
||||
};
|
||||
|
||||
const handleAddChainedDeadline = async (entryId: string, typeId: string, startDate: string, parentId: string) => {
|
||||
const handleAddChainedDeadline = async (
|
||||
entryId: string,
|
||||
typeId: string,
|
||||
startDate: string,
|
||||
parentId: string,
|
||||
) => {
|
||||
await addDeadline(entryId, typeId, startDate, parentId);
|
||||
};
|
||||
|
||||
// Stats
|
||||
const total = allEntries.length;
|
||||
const open = allEntries.filter((e) => e.status === 'deschis').length;
|
||||
const open = allEntries.filter((e) => e.status === "deschis").length;
|
||||
const overdue = allEntries.filter((e) => {
|
||||
if (e.status !== 'deschis') return false;
|
||||
if (e.status !== "deschis") return false;
|
||||
const days = getOverdueDays(e.deadline);
|
||||
return days !== null && days > 0;
|
||||
}).length;
|
||||
const intrat = allEntries.filter((e) => e.direction === 'intrat').length;
|
||||
const intrat = allEntries.filter((e) => e.direction === "intrat").length;
|
||||
|
||||
const deadlineStats = useMemo(() => aggregateDeadlines(allEntries), [allEntries]);
|
||||
const deadlineStats = useMemo(
|
||||
() => aggregateDeadlines(allEntries),
|
||||
[allEntries],
|
||||
);
|
||||
const urgentDeadlines = deadlineStats.urgent + deadlineStats.overdue;
|
||||
|
||||
const closingEntry = closingId ? allEntries.find((e) => e.id === closingId) : null;
|
||||
const closingEntry = closingId
|
||||
? allEntries.find((e) => e.id === closingId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="registru">
|
||||
@@ -110,7 +209,10 @@ export function RegistraturaModule() {
|
||||
<TabsTrigger value="termene">
|
||||
Termene legale
|
||||
{urgentDeadlines > 0 && (
|
||||
<Badge variant="destructive" className="ml-1.5 text-[10px] px-1.5 py-0">
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="ml-1.5 text-[10px] px-1.5 py-0"
|
||||
>
|
||||
{urgentDeadlines}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -123,15 +225,19 @@ export function RegistraturaModule() {
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Total" value={total} />
|
||||
<StatCard label="Deschise" value={open} />
|
||||
<StatCard label="Depășite" value={overdue} variant={overdue > 0 ? 'destructive' : undefined} />
|
||||
<StatCard
|
||||
label="Depășite"
|
||||
value={overdue}
|
||||
variant={overdue > 0 ? "destructive" : undefined}
|
||||
/>
|
||||
<StatCard label="Intrate" value={intrat} />
|
||||
</div>
|
||||
|
||||
{viewMode === 'list' && (
|
||||
{viewMode === "list" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<RegistryFilters filters={filters} onUpdate={updateFilter} />
|
||||
<Button onClick={() => setViewMode('add')} className="shrink-0">
|
||||
<Button onClick={() => setViewMode("add")} className="shrink-0">
|
||||
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
||||
</Button>
|
||||
</div>
|
||||
@@ -152,12 +258,14 @@ export function RegistraturaModule() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{viewMode === 'add' && (
|
||||
{viewMode === "add" && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Înregistrare nouă
|
||||
<Badge variant="outline" className="text-xs">Nr. auto</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Nr. auto
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -165,12 +273,14 @@ export function RegistraturaModule() {
|
||||
allEntries={allEntries}
|
||||
onSubmit={handleAdd}
|
||||
onCancel={handleCancel}
|
||||
onCreateContact={handleCreateContact}
|
||||
onCreateDocType={handleCreateDocType}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{viewMode === 'edit' && editingEntry && (
|
||||
{viewMode === "edit" && editingEntry && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Editare — {editingEntry.number}</CardTitle>
|
||||
@@ -181,26 +291,40 @@ export function RegistraturaModule() {
|
||||
allEntries={allEntries}
|
||||
onSubmit={handleUpdate}
|
||||
onCancel={handleCancel}
|
||||
onCreateContact={handleCreateContact}
|
||||
onCreateDocType={handleCreateDocType}
|
||||
onNavigateEntry={handleNavigateEntry}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Close confirmation dialog */}
|
||||
<Dialog open={closingId !== null} onOpenChange={(open) => { if (!open) setClosingId(null); }}>
|
||||
<Dialog
|
||||
open={closingId !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setClosingId(null);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Închide înregistrarea</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
<p className="text-sm">
|
||||
Această înregistrare are {closingEntry?.linkedEntryIds?.length ?? 0} înregistrări legate.
|
||||
Vrei să le închizi și pe acestea?
|
||||
Această înregistrare are{" "}
|
||||
{closingEntry?.linkedEntryIds?.length ?? 0} înregistrări
|
||||
legate. Vrei să le închizi și pe acestea?
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setClosingId(null)}>Anulează</Button>
|
||||
<Button variant="secondary" onClick={() => handleCloseConfirm(false)}>
|
||||
<Button variant="outline" onClick={() => setClosingId(null)}>
|
||||
Anulează
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleCloseConfirm(false)}
|
||||
>
|
||||
Doar aceasta
|
||||
</Button>
|
||||
<Button onClick={() => handleCloseConfirm(true)}>
|
||||
@@ -223,12 +347,22 @@ export function RegistraturaModule() {
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' }) {
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
variant,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
variant?: "destructive";
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className={`text-2xl font-bold ${variant === 'destructive' && value > 0 ? 'text-destructive' : ''}`}>
|
||||
<p
|
||||
className={`text-2xl font-bold ${variant === "destructive" && value > 0 ? "text-destructive" : ""}`}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { Paperclip, X, Clock, Plus } from "lucide-react";
|
||||
import { useState, useMemo, useRef, useCallback } from "react";
|
||||
import {
|
||||
Paperclip,
|
||||
X,
|
||||
Clock,
|
||||
Plus,
|
||||
UserPlus,
|
||||
Info,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
import type {
|
||||
RegistryEntry,
|
||||
RegistryDirection,
|
||||
RegistryStatus,
|
||||
DocumentType,
|
||||
RegistryAttachment,
|
||||
TrackedDeadline,
|
||||
DeadlineResolution,
|
||||
} from "../types";
|
||||
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { Textarea } from "@/shared/components/ui/textarea";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import { Switch } from "@/shared/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -24,11 +33,21 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/components/ui/tooltip";
|
||||
import { useContacts } from "@/modules/address-book/hooks/use-contacts";
|
||||
import { useTags } from "@/core/tagging";
|
||||
import type { AddressContact } from "@/modules/address-book/types";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { DeadlineCard } from "./deadline-card";
|
||||
import { DeadlineAddDialog } from "./deadline-add-dialog";
|
||||
import { DeadlineResolveDialog } from "./deadline-resolve-dialog";
|
||||
import { QuickContactDialog } from "./quick-contact-dialog";
|
||||
import { ThreadView } from "./thread-view";
|
||||
import {
|
||||
createTrackedDeadline,
|
||||
resolveDeadline as resolveDeadlineFn,
|
||||
@@ -42,35 +61,55 @@ interface RegistryEntryFormProps {
|
||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||
) => void;
|
||||
onCancel: () => void;
|
||||
/** Callback to create a new Address Book contact */
|
||||
onCreateContact?: (data: {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}) => Promise<AddressContact | undefined>;
|
||||
/** Callback to create a new document type tag in Tag Manager */
|
||||
onCreateDocType?: (label: string) => Promise<void>;
|
||||
/** Navigate to an entry (for thread clicks) */
|
||||
onNavigateEntry?: (entry: RegistryEntry) => void;
|
||||
}
|
||||
|
||||
const DOC_TYPE_LABELS: Record<DocumentType, string> = {
|
||||
contract: "Contract",
|
||||
oferta: "Ofertă",
|
||||
factura: "Factură",
|
||||
scrisoare: "Scrisoare",
|
||||
aviz: "Aviz",
|
||||
"nota-de-comanda": "Notă de comandă",
|
||||
raport: "Raport",
|
||||
cerere: "Cerere",
|
||||
altele: "Altele",
|
||||
};
|
||||
|
||||
export function RegistryEntryForm({
|
||||
initial,
|
||||
allEntries,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onCreateContact,
|
||||
onCreateDocType,
|
||||
onNavigateEntry,
|
||||
}: RegistryEntryFormProps) {
|
||||
const { allContacts } = useContacts();
|
||||
const { allContacts, refresh: refreshContacts } = useContacts();
|
||||
const { tags: docTypeTags } = useTags("document-type");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// ── Build dynamic doc type list from defaults + Tag Manager ──
|
||||
const allDocTypes = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
// Add defaults
|
||||
for (const [key, label] of Object.entries(DEFAULT_DOC_TYPE_LABELS)) {
|
||||
map.set(key, label);
|
||||
}
|
||||
// Add from Tag Manager (document-type category)
|
||||
for (const tag of docTypeTags) {
|
||||
const key = tag.label.toLowerCase().replace(/\s+/g, "-");
|
||||
if (!map.has(key)) {
|
||||
map.set(key, tag.label);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [docTypeTags]);
|
||||
|
||||
const [direction, setDirection] = useState<RegistryDirection>(
|
||||
initial?.direction ?? "intrat",
|
||||
);
|
||||
const [documentType, setDocumentType] = useState<DocumentType>(
|
||||
initial?.documentType ?? "scrisoare",
|
||||
);
|
||||
const [customDocType, setCustomDocType] = useState("");
|
||||
const [subject, setSubject] = useState(initial?.subject ?? "");
|
||||
const [date, setDate] = useState(
|
||||
initial?.date ?? new Date().toISOString().slice(0, 10),
|
||||
@@ -86,10 +125,15 @@ export function RegistryEntryForm({
|
||||
const [company, setCompany] = useState<CompanyId>(
|
||||
initial?.company ?? "beletage",
|
||||
);
|
||||
const [status, setStatus] = useState<RegistryStatus>(
|
||||
initial?.status ?? "deschis",
|
||||
);
|
||||
const [isClosed, setIsClosed] = useState(initial?.status === "inchis");
|
||||
const [deadline, setDeadline] = useState(initial?.deadline ?? "");
|
||||
const [assignee, setAssignee] = useState(initial?.assignee ?? "");
|
||||
const [assigneeContactId, setAssigneeContactId] = useState(
|
||||
initial?.assigneeContactId ?? "",
|
||||
);
|
||||
const [threadParentId, setThreadParentId] = useState(
|
||||
initial?.threadParentId ?? "",
|
||||
);
|
||||
const [notes, setNotes] = useState(initial?.notes ?? "");
|
||||
const [linkedEntryIds, setLinkedEntryIds] = useState<string[]>(
|
||||
initial?.linkedEntryIds ?? [],
|
||||
@@ -101,12 +145,20 @@ export function RegistryEntryForm({
|
||||
initial?.trackedDeadlines ?? [],
|
||||
);
|
||||
const [linkedSearch, setLinkedSearch] = useState("");
|
||||
const [threadSearch, setThreadSearch] = useState("");
|
||||
|
||||
// ── Deadline dialogs ──
|
||||
const [deadlineAddOpen, setDeadlineAddOpen] = useState(false);
|
||||
const [resolvingDeadline, setResolvingDeadline] =
|
||||
useState<TrackedDeadline | null>(null);
|
||||
|
||||
// ── Quick contact creation ──
|
||||
const [quickContactOpen, setQuickContactOpen] = useState(false);
|
||||
const [quickContactField, setQuickContactField] = useState<
|
||||
"sender" | "recipient" | "assignee"
|
||||
>("sender");
|
||||
const [quickContactName, setQuickContactName] = useState("");
|
||||
|
||||
const handleAddDeadline = (
|
||||
typeId: string,
|
||||
startDate: string,
|
||||
@@ -127,7 +179,6 @@ export function RegistryEntryForm({
|
||||
prev.map((d) => (d.id === resolved.id ? resolved : d)),
|
||||
);
|
||||
|
||||
// Handle chain
|
||||
if (chainNext) {
|
||||
const def = getDeadlineType(resolvingDeadline.typeId);
|
||||
if (def?.chainNextTypeId) {
|
||||
@@ -147,33 +198,86 @@ export function RegistryEntryForm({
|
||||
setTrackedDeadlines((prev) => prev.filter((d) => d.id !== deadlineId));
|
||||
};
|
||||
|
||||
// ── Sender/Recipient autocomplete suggestions ──
|
||||
// ── Contact autocomplete ──
|
||||
const [senderFocused, setSenderFocused] = useState(false);
|
||||
const [recipientFocused, setRecipientFocused] = useState(false);
|
||||
const [assigneeFocused, setAssigneeFocused] = useState(false);
|
||||
|
||||
const senderSuggestions = useMemo(() => {
|
||||
if (!sender || sender.length < 2) return [];
|
||||
const q = sender.toLowerCase();
|
||||
return allContacts
|
||||
.filter(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
c.company.toLowerCase().includes(q),
|
||||
)
|
||||
.slice(0, 5);
|
||||
}, [allContacts, sender]);
|
||||
const filterContacts = useCallback(
|
||||
(query: string) => {
|
||||
if (!query || query.length < 2) return [];
|
||||
const q = query.toLowerCase();
|
||||
return allContacts
|
||||
.filter(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
c.company.toLowerCase().includes(q),
|
||||
)
|
||||
.slice(0, 5);
|
||||
},
|
||||
[allContacts],
|
||||
);
|
||||
|
||||
const recipientSuggestions = useMemo(() => {
|
||||
if (!recipient || recipient.length < 2) return [];
|
||||
const q = recipient.toLowerCase();
|
||||
return allContacts
|
||||
.filter(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
c.company.toLowerCase().includes(q),
|
||||
)
|
||||
.slice(0, 5);
|
||||
}, [allContacts, recipient]);
|
||||
const senderSuggestions = useMemo(
|
||||
() => filterContacts(sender),
|
||||
[filterContacts, sender],
|
||||
);
|
||||
const recipientSuggestions = useMemo(
|
||||
() => filterContacts(recipient),
|
||||
[filterContacts, recipient],
|
||||
);
|
||||
const assigneeSuggestions = useMemo(
|
||||
() => filterContacts(assignee),
|
||||
[filterContacts, assignee],
|
||||
);
|
||||
|
||||
// ── Quick contact creation handler ──
|
||||
const openQuickContact = (
|
||||
field: "sender" | "recipient" | "assignee",
|
||||
name: string,
|
||||
) => {
|
||||
setQuickContactField(field);
|
||||
setQuickContactName(name);
|
||||
setQuickContactOpen(true);
|
||||
};
|
||||
|
||||
const handleQuickContactConfirm = async (data: {
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}) => {
|
||||
if (!onCreateContact) return;
|
||||
const contact = await onCreateContact(data);
|
||||
if (contact) {
|
||||
const displayName = contact.company
|
||||
? `${contact.name} (${contact.company})`
|
||||
: contact.name;
|
||||
if (quickContactField === "sender") {
|
||||
setSender(displayName);
|
||||
setSenderContactId(contact.id);
|
||||
} else if (quickContactField === "recipient") {
|
||||
setRecipient(displayName);
|
||||
setRecipientContactId(contact.id);
|
||||
} else {
|
||||
setAssignee(displayName);
|
||||
setAssigneeContactId(contact.id);
|
||||
}
|
||||
await refreshContacts();
|
||||
}
|
||||
setQuickContactOpen(false);
|
||||
};
|
||||
|
||||
// ── Custom doc type creation ──
|
||||
const handleAddCustomDocType = async () => {
|
||||
const label = customDocType.trim();
|
||||
if (!label) return;
|
||||
const key = label.toLowerCase().replace(/\s+/g, "-");
|
||||
setDocumentType(key);
|
||||
setCustomDocType("");
|
||||
if (onCreateDocType) {
|
||||
await onCreateDocType(label);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
@@ -215,8 +319,11 @@ export function RegistryEntryForm({
|
||||
recipient,
|
||||
recipientContactId: recipientContactId || undefined,
|
||||
company,
|
||||
status,
|
||||
status: isClosed ? "inchis" : "deschis",
|
||||
deadline: deadline || undefined,
|
||||
assignee: assignee || undefined,
|
||||
assigneeContactId: assigneeContactId || undefined,
|
||||
threadParentId: threadParentId || undefined,
|
||||
linkedEntryIds,
|
||||
attachments,
|
||||
trackedDeadlines:
|
||||
@@ -227,8 +334,78 @@ export function RegistryEntryForm({
|
||||
});
|
||||
};
|
||||
|
||||
// ── Contact autocomplete dropdown renderer ──
|
||||
const renderContactDropdown = (
|
||||
suggestions: AddressContact[],
|
||||
focused: boolean,
|
||||
fieldName: "sender" | "recipient" | "assignee",
|
||||
currentValue: string,
|
||||
onSelect: (c: AddressContact) => void,
|
||||
) => {
|
||||
if (!focused) return null;
|
||||
|
||||
const hasExactMatch = suggestions.some(
|
||||
(c) =>
|
||||
c.name.toLowerCase() === currentValue.toLowerCase() ||
|
||||
`${c.name} (${c.company})`.toLowerCase() === currentValue.toLowerCase(),
|
||||
);
|
||||
|
||||
const showCreateButton =
|
||||
currentValue.length >= 2 && !hasExactMatch && onCreateContact;
|
||||
|
||||
if (suggestions.length === 0 && !showCreateButton) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
|
||||
{suggestions.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
|
||||
onMouseDown={() => onSelect(c)}
|
||||
>
|
||||
<span className="font-medium">{c.name}</span>
|
||||
{c.company && (
|
||||
<span className="ml-1 text-muted-foreground text-xs">
|
||||
{c.company}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{showCreateButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent text-primary flex items-center gap-1.5 border-t mt-1 pt-1.5"
|
||||
onMouseDown={() => openQuickContact(fieldName, currentValue)}
|
||||
>
|
||||
<UserPlus className="h-3.5 w-3.5" />
|
||||
Creează contact "{currentValue}"
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Thread parent entry for display
|
||||
const threadParent =
|
||||
threadParentId && allEntries
|
||||
? allEntries.find((e) => e.id === threadParentId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Thread view (if editing an entry that's in a thread) */}
|
||||
{initial &&
|
||||
allEntries &&
|
||||
(initial.threadParentId ||
|
||||
allEntries.some((e) => e.threadParentId === initial.id)) && (
|
||||
<ThreadView
|
||||
entry={initial}
|
||||
allEntries={allEntries}
|
||||
onNavigate={onNavigateEntry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Row 1: Direction + Document type + Date */}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
@@ -248,23 +425,48 @@ export function RegistryEntryForm({
|
||||
</div>
|
||||
<div>
|
||||
<Label>Tip document</Label>
|
||||
<Select
|
||||
value={documentType}
|
||||
onValueChange={(v) => setDocumentType(v as DocumentType)}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
Object.entries(DOC_TYPE_LABELS) as [DocumentType, string][]
|
||||
).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="mt-1">
|
||||
<Select
|
||||
value={allDocTypes.has(documentType) ? documentType : "altele"}
|
||||
onValueChange={(v) => setDocumentType(v as DocumentType)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from(allDocTypes.entries()).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Add custom type inline */}
|
||||
<div className="mt-1.5 flex gap-1">
|
||||
<Input
|
||||
value={customDocType}
|
||||
onChange={(e) => setCustomDocType(e.target.value)}
|
||||
placeholder="Tip nou..."
|
||||
className="text-xs h-7"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleAddCustomDocType();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2"
|
||||
onClick={handleAddCustomDocType}
|
||||
disabled={!customDocType.trim()}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Data</Label>
|
||||
@@ -288,8 +490,9 @@ export function RegistryEntryForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sender / Recipient with autocomplete */}
|
||||
{/* Sender / Recipient with autocomplete + quick create */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Sender */}
|
||||
<div className="relative">
|
||||
<Label>Expeditor</Label>
|
||||
<Input
|
||||
@@ -303,30 +506,19 @@ export function RegistryEntryForm({
|
||||
className="mt-1"
|
||||
placeholder="Nume sau companie..."
|
||||
/>
|
||||
{senderFocused && senderSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
|
||||
{senderSuggestions.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
|
||||
onMouseDown={() => {
|
||||
setSender(c.company ? `${c.name} (${c.company})` : c.name);
|
||||
setSenderContactId(c.id);
|
||||
setSenderFocused(false);
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">{c.name}</span>
|
||||
{c.company && (
|
||||
<span className="ml-1 text-muted-foreground text-xs">
|
||||
{c.company}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{renderContactDropdown(
|
||||
senderSuggestions,
|
||||
senderFocused,
|
||||
"sender",
|
||||
sender,
|
||||
(c) => {
|
||||
setSender(c.company ? `${c.name} (${c.company})` : c.name);
|
||||
setSenderContactId(c.id);
|
||||
setSenderFocused(false);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
{/* Recipient */}
|
||||
<div className="relative">
|
||||
<Label>Destinatar</Label>
|
||||
<Input
|
||||
@@ -340,35 +532,63 @@ export function RegistryEntryForm({
|
||||
className="mt-1"
|
||||
placeholder="Nume sau companie..."
|
||||
/>
|
||||
{recipientFocused && recipientSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-md border bg-popover p-1 shadow-md">
|
||||
{recipientSuggestions.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
className="w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
|
||||
onMouseDown={() => {
|
||||
setRecipient(
|
||||
c.company ? `${c.name} (${c.company})` : c.name,
|
||||
);
|
||||
setRecipientContactId(c.id);
|
||||
setRecipientFocused(false);
|
||||
}}
|
||||
>
|
||||
<span className="font-medium">{c.name}</span>
|
||||
{c.company && (
|
||||
<span className="ml-1 text-muted-foreground text-xs">
|
||||
{c.company}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{renderContactDropdown(
|
||||
recipientSuggestions,
|
||||
recipientFocused,
|
||||
"recipient",
|
||||
recipient,
|
||||
(c) => {
|
||||
setRecipient(c.company ? `${c.name} (${c.company})` : c.name);
|
||||
setRecipientContactId(c.id);
|
||||
setRecipientFocused(false);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Company + Status + Deadline */}
|
||||
{/* Assignee (Responsabil) */}
|
||||
<div className="relative">
|
||||
<Label className="flex items-center gap-1.5">
|
||||
Responsabil
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Persoana din echipă responsabilă cu gestionarea acestei
|
||||
înregistrări. Câmp pregătit pentru integrare viitoare cu ERP.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
value={assignee}
|
||||
onChange={(e) => {
|
||||
setAssignee(e.target.value);
|
||||
setAssigneeContactId("");
|
||||
}}
|
||||
onFocus={() => setAssigneeFocused(true)}
|
||||
onBlur={() => setTimeout(() => setAssigneeFocused(false), 200)}
|
||||
className="mt-1"
|
||||
placeholder="Persoana responsabilă..."
|
||||
/>
|
||||
{renderContactDropdown(
|
||||
assigneeSuggestions,
|
||||
assigneeFocused,
|
||||
"assignee",
|
||||
assignee,
|
||||
(c) => {
|
||||
setAssignee(c.company ? `${c.name} (${c.company})` : c.name);
|
||||
setAssigneeContactId(c.id);
|
||||
setAssigneeFocused(false);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Company + Closed switch + Deadline */}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<Label>Companie</Label>
|
||||
@@ -388,22 +608,47 @@ export function RegistryEntryForm({
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v as RegistryStatus)}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="deschis">Deschis</SelectItem>
|
||||
<SelectItem value="inchis">Închis</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="flex items-center gap-1.5">
|
||||
Închis
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Implicit, toate înregistrările sunt deschise. Bifează doar
|
||||
când dosarul este finalizat.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<div className="mt-2.5 flex items-center gap-2">
|
||||
<Switch checked={isClosed} onCheckedChange={setIsClosed} />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{isClosed ? "Închis" : "Deschis"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Termen limită</Label>
|
||||
<Label className="flex items-center gap-1.5">
|
||||
Termen limită intern
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Termen intern pentru a răspunde sau a acționa pe această
|
||||
înregistrare. Nu este termen legal — termenele legale se
|
||||
adaugă separat mai jos.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={deadline}
|
||||
@@ -413,6 +658,105 @@ export function RegistryEntryForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thread parent — reply to another entry */}
|
||||
{allEntries && allEntries.length > 0 && (
|
||||
<div>
|
||||
<Label className="flex items-center gap-1.5">
|
||||
<GitBranch className="h-3.5 w-3.5" />
|
||||
Răspuns la (Thread)
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Leagă această înregistrare ca răspuns la alta, creând un fir
|
||||
de conversație instituțională. O intrare poate genera mai
|
||||
multe ieșiri (branching).
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
{threadParent && (
|
||||
<div className="mt-1.5 flex items-center gap-2 rounded border border-primary/30 bg-primary/5 px-2 py-1.5 text-sm">
|
||||
<Badge
|
||||
variant={
|
||||
threadParent.direction === "intrat" ? "default" : "secondary"
|
||||
}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{threadParent.direction === "intrat" ? "↓" : "↑"}
|
||||
</Badge>
|
||||
<span className="font-mono text-xs">{threadParent.number}</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{threadParent.subject}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-auto h-6 w-6 p-0"
|
||||
onClick={() => setThreadParentId("")}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!threadParentId && (
|
||||
<>
|
||||
<Input
|
||||
className="mt-1.5"
|
||||
placeholder="Caută după număr sau subiect..."
|
||||
value={threadSearch}
|
||||
onChange={(e) => setThreadSearch(e.target.value)}
|
||||
/>
|
||||
{threadSearch.trim().length >= 2 && (
|
||||
<div className="mt-1.5 max-h-32 overflow-y-auto space-y-1">
|
||||
{allEntries
|
||||
.filter((e) => {
|
||||
if (e.id === initial?.id) return false;
|
||||
const q = threadSearch.toLowerCase();
|
||||
return (
|
||||
e.number.toLowerCase().includes(q) ||
|
||||
e.subject.toLowerCase().includes(q)
|
||||
);
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map((e) => (
|
||||
<button
|
||||
key={e.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setThreadParentId(e.id);
|
||||
setThreadSearch("");
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded border px-2 py-1 text-xs transition-colors hover:bg-accent"
|
||||
>
|
||||
<Badge
|
||||
variant={
|
||||
e.direction === "intrat" ? "default" : "secondary"
|
||||
}
|
||||
className="text-[9px] px-1 py-0"
|
||||
>
|
||||
{e.direction === "intrat" ? "↓" : "↑"}
|
||||
</Badge>
|
||||
<span className="font-mono">{e.number}</span>
|
||||
<span className="truncate text-muted-foreground">
|
||||
{e.subject.length > 40
|
||||
? e.subject.slice(0, 40) + "…"
|
||||
: e.subject}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Linked entries */}
|
||||
{allEntries && allEntries.length > 0 && (
|
||||
<div>
|
||||
@@ -582,6 +926,14 @@ export function RegistryEntryForm({
|
||||
</Button>
|
||||
<Button type="submit">{initial ? "Actualizează" : "Adaugă"}</Button>
|
||||
</div>
|
||||
|
||||
{/* Quick contact creation dialog */}
|
||||
<QuickContactDialog
|
||||
open={quickContactOpen}
|
||||
onOpenChange={setQuickContactOpen}
|
||||
initialName={quickContactName}
|
||||
onConfirm={handleQuickContactConfirm}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Search } from 'lucide-react';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
||||
import type { RegistryFilters as Filters } from '../hooks/use-registry';
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select";
|
||||
import type { RegistryFilters as Filters } from "../hooks/use-registry";
|
||||
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
||||
|
||||
interface RegistryFiltersProps {
|
||||
filters: Filters;
|
||||
onUpdate: <K extends keyof Filters>(key: K, value: Filters[K]) => void;
|
||||
}
|
||||
|
||||
const DOC_TYPE_LABELS: Record<string, string> = {
|
||||
contract: 'Contract',
|
||||
oferta: 'Ofertă',
|
||||
factura: 'Factură',
|
||||
scrisoare: 'Scrisoare',
|
||||
aviz: 'Aviz',
|
||||
'nota-de-comanda': 'Notă de comandă',
|
||||
raport: 'Raport',
|
||||
cerere: 'Cerere',
|
||||
altele: 'Altele',
|
||||
};
|
||||
|
||||
export function RegistryFilters({ filters, onUpdate }: RegistryFiltersProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
@@ -30,12 +25,15 @@ export function RegistryFilters({ filters, onUpdate }: RegistryFiltersProps) {
|
||||
<Input
|
||||
placeholder="Caută după subiect, expeditor, număr..."
|
||||
value={filters.search}
|
||||
onChange={(e) => onUpdate('search', e.target.value)}
|
||||
onChange={(e) => onUpdate("search", e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={filters.direction} onValueChange={(v) => onUpdate('direction', v as Filters['direction'])}>
|
||||
<Select
|
||||
value={filters.direction}
|
||||
onValueChange={(v) => onUpdate("direction", v as Filters["direction"])}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="Direcție" />
|
||||
</SelectTrigger>
|
||||
@@ -46,19 +44,29 @@ export function RegistryFilters({ filters, onUpdate }: RegistryFiltersProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filters.documentType} onValueChange={(v) => onUpdate('documentType', v as Filters['documentType'])}>
|
||||
<Select
|
||||
value={filters.documentType}
|
||||
onValueChange={(v) =>
|
||||
onUpdate("documentType", v as Filters["documentType"])
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="Tip document" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toate tipurile</SelectItem>
|
||||
{Object.entries(DOC_TYPE_LABELS).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>{label}</SelectItem>
|
||||
{Object.entries(DEFAULT_DOC_TYPE_LABELS).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filters.status} onValueChange={(v) => onUpdate('status', v as Filters['status'])}>
|
||||
<Select
|
||||
value={filters.status}
|
||||
onValueChange={(v) => onUpdate("status", v as Filters["status"])}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
@@ -69,7 +77,10 @@ export function RegistryFilters({ filters, onUpdate }: RegistryFiltersProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filters.company} onValueChange={(v) => onUpdate('company', v)}>
|
||||
<Select
|
||||
value={filters.company}
|
||||
onValueChange={(v) => onUpdate("company", v)}
|
||||
>
|
||||
<SelectTrigger className="w-[170px]">
|
||||
<SelectValue placeholder="Companie" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Pencil, Trash2, CheckCircle2, Link2, Clock } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import type { RegistryEntry, DocumentType } from '../types';
|
||||
import { getOverdueDays } from '../services/registry-service';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import {
|
||||
Pencil,
|
||||
Trash2,
|
||||
CheckCircle2,
|
||||
Link2,
|
||||
Clock,
|
||||
GitBranch,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import type { RegistryEntry } from "../types";
|
||||
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
|
||||
import { getOverdueDays } from "../services/registry-service";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
interface RegistryTableProps {
|
||||
entries: RegistryEntry[];
|
||||
@@ -16,30 +25,36 @@ interface RegistryTableProps {
|
||||
}
|
||||
|
||||
const DIRECTION_LABELS: Record<string, string> = {
|
||||
intrat: 'Intrat',
|
||||
iesit: 'Ieșit',
|
||||
intrat: "Intrat",
|
||||
iesit: "Ieșit",
|
||||
};
|
||||
|
||||
const DOC_TYPE_LABELS: Record<DocumentType, string> = {
|
||||
contract: 'Contract',
|
||||
oferta: 'Ofertă',
|
||||
factura: 'Factură',
|
||||
scrisoare: 'Scrisoare',
|
||||
aviz: 'Aviz',
|
||||
'nota-de-comanda': 'Notă comandă',
|
||||
raport: 'Raport',
|
||||
cerere: 'Cerere',
|
||||
altele: 'Altele',
|
||||
};
|
||||
/** Resolve doc type label from defaults or capitalize custom type */
|
||||
function getDocTypeLabel(type: string): string {
|
||||
const label = DEFAULT_DOC_TYPE_LABELS[type];
|
||||
if (label) return label;
|
||||
// For custom types, capitalize first letter
|
||||
return type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
deschis: 'Deschis',
|
||||
inchis: 'Închis',
|
||||
deschis: "Deschis",
|
||||
inchis: "Închis",
|
||||
};
|
||||
|
||||
export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: RegistryTableProps) {
|
||||
export function RegistryTable({
|
||||
entries,
|
||||
loading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: RegistryTableProps) {
|
||||
if (loading) {
|
||||
return <p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>;
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
Se încarcă...
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
@@ -62,6 +77,7 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
|
||||
<th className="px-3 py-2 text-left font-medium">Subiect</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Expeditor</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Destinatar</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Resp.</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Termen</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Status</th>
|
||||
<th className="px-3 py-2 text-right font-medium">Acțiuni</th>
|
||||
@@ -69,29 +85,45 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => {
|
||||
const overdueDays = (entry.status === 'deschis' || !entry.status) ? getOverdueDays(entry.deadline) : null;
|
||||
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',
|
||||
isOverdue && 'bg-destructive/5'
|
||||
"border-b transition-colors hover:bg-muted/20",
|
||||
isOverdue && "bg-destructive/5",
|
||||
)}
|
||||
>
|
||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{entry.number}</td>
|
||||
<td className="px-3 py-2 text-xs whitespace-nowrap">{formatDate(entry.date)}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">
|
||||
{entry.number}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs whitespace-nowrap">
|
||||
{formatDate(entry.date)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge
|
||||
variant={entry.direction === 'intrat' ? 'default' : 'secondary'}
|
||||
variant={
|
||||
entry.direction === "intrat" ? "default" : "secondary"
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{DIRECTION_LABELS[entry.direction] ?? entry.direction ?? '—'}
|
||||
{DIRECTION_LABELS[entry.direction] ??
|
||||
entry.direction ??
|
||||
"—"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs">{DOC_TYPE_LABELS[entry.documentType] ?? entry.documentType ?? '—'}</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 && (
|
||||
<Link2 className="ml-1 inline h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -107,17 +139,39 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
|
||||
</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-[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')}>
|
||||
<span
|
||||
className={cn(
|
||||
isOverdue && "font-medium text-destructive",
|
||||
)}
|
||||
>
|
||||
{formatDate(entry.deadline)}
|
||||
{overdueDays !== null && overdueDays > 0 && (
|
||||
<span className="ml-1 text-[10px]">({overdueDays}z depășit)</span>
|
||||
<span className="ml-1 text-[10px]">
|
||||
({overdueDays}z depășit)
|
||||
</span>
|
||||
)}
|
||||
{overdueDays !== null && overdueDays < 0 && (
|
||||
<span className="ml-1 text-[10px] text-muted-foreground">({Math.abs(overdueDays)}z)</span>
|
||||
<span className="ml-1 text-[10px] text-muted-foreground">
|
||||
({Math.abs(overdueDays)}z)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
@@ -125,21 +179,39 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge variant={entry.status === 'deschis' ? 'default' : 'outline'}>
|
||||
<Badge
|
||||
variant={entry.status === "deschis" ? "default" : "outline"}
|
||||
>
|
||||
{STATUS_LABELS[entry.status]}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
{entry.status === 'deschis' && (
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-green-600" onClick={() => onClose(entry.id)} title="Închide">
|
||||
{entry.status === "deschis" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-green-600"
|
||||
onClick={() => onClose(entry.id)}
|
||||
title="Închide"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onEdit(entry)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => onEdit(entry)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => onDelete(entry.id)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={() => onDelete(entry.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -155,7 +227,11 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
return new Date(iso).toLocaleDateString("ro-RO", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
export { registraturaConfig } from './config';
|
||||
export { RegistraturaModule } from './components/registratura-module';
|
||||
export { registraturaConfig } from "./config";
|
||||
export { RegistraturaModule } from "./components/registratura-module";
|
||||
export type {
|
||||
RegistryEntry, RegistryDirection, RegistryStatus, DocumentType,
|
||||
DeadlineDayType, DeadlineResolution, DeadlineCategory,
|
||||
DeadlineTypeDef, TrackedDeadline,
|
||||
} from './types';
|
||||
RegistryEntry,
|
||||
RegistryDirection,
|
||||
RegistryStatus,
|
||||
DocumentType,
|
||||
DeadlineDayType,
|
||||
DeadlineResolution,
|
||||
DeadlineCategory,
|
||||
DeadlineTypeDef,
|
||||
TrackedDeadline,
|
||||
} from "./types";
|
||||
export { DEFAULT_DOCUMENT_TYPES, DEFAULT_DOC_TYPE_LABELS } from "./types";
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
import type { Visibility } from '@/core/module-registry/types';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
import type { Visibility } from "@/core/module-registry/types";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
|
||||
/** Document direction — simplified from the old 3-way type */
|
||||
export type RegistryDirection = 'intrat' | 'iesit';
|
||||
export type RegistryDirection = "intrat" | "iesit";
|
||||
|
||||
/** Document type categories */
|
||||
export type DocumentType =
|
||||
| 'contract'
|
||||
| 'oferta'
|
||||
| 'factura'
|
||||
| 'scrisoare'
|
||||
| 'aviz'
|
||||
| 'nota-de-comanda'
|
||||
| 'raport'
|
||||
| 'cerere'
|
||||
| 'altele';
|
||||
/** Default document types — user can add custom types that sync with Tag Manager */
|
||||
export const DEFAULT_DOCUMENT_TYPES = [
|
||||
"contract",
|
||||
"oferta",
|
||||
"factura",
|
||||
"scrisoare",
|
||||
"aviz",
|
||||
"nota-de-comanda",
|
||||
"raport",
|
||||
"cerere",
|
||||
"apel-telefonic",
|
||||
"videoconferinta",
|
||||
"altele",
|
||||
] as const;
|
||||
|
||||
/** Document type — string-based for dynamic types from Tag Manager */
|
||||
export type DocumentType = (typeof DEFAULT_DOCUMENT_TYPES)[number] | string;
|
||||
|
||||
/** Labels for default document types */
|
||||
export const DEFAULT_DOC_TYPE_LABELS: Record<string, string> = {
|
||||
contract: "Contract",
|
||||
oferta: "Ofertă",
|
||||
factura: "Factură",
|
||||
scrisoare: "Scrisoare",
|
||||
aviz: "Aviz",
|
||||
"nota-de-comanda": "Notă de comandă",
|
||||
raport: "Raport",
|
||||
cerere: "Cerere",
|
||||
"apel-telefonic": "Apel telefonic",
|
||||
videoconferinta: "Videoconferință",
|
||||
altele: "Altele",
|
||||
};
|
||||
|
||||
/** Status — simplified to open/closed */
|
||||
export type RegistryStatus = 'deschis' | 'inchis';
|
||||
export type RegistryStatus = "deschis" | "inchis";
|
||||
|
||||
/** File attachment */
|
||||
export interface RegistryAttachment {
|
||||
@@ -32,11 +53,21 @@ export interface RegistryAttachment {
|
||||
|
||||
// ── Deadline tracking types ──
|
||||
|
||||
export type DeadlineDayType = 'calendar' | 'working';
|
||||
export type DeadlineDayType = "calendar" | "working";
|
||||
|
||||
export type DeadlineResolution = 'pending' | 'completed' | 'aprobat-tacit' | 'respins' | 'anulat';
|
||||
export type DeadlineResolution =
|
||||
| "pending"
|
||||
| "completed"
|
||||
| "aprobat-tacit"
|
||||
| "respins"
|
||||
| "anulat";
|
||||
|
||||
export type DeadlineCategory = 'avize' | 'completari' | 'analiza' | 'autorizare' | 'publicitate';
|
||||
export type DeadlineCategory =
|
||||
| "avize"
|
||||
| "completari"
|
||||
| "analiza"
|
||||
| "autorizare"
|
||||
| "publicitate";
|
||||
|
||||
export interface DeadlineTypeDef {
|
||||
id: string;
|
||||
@@ -86,6 +117,11 @@ export interface RegistryEntry {
|
||||
status: RegistryStatus;
|
||||
/** Deadline date (YYYY-MM-DD) */
|
||||
deadline?: string;
|
||||
/** Assignee — person responsible (ERP-ready field) */
|
||||
assignee?: string;
|
||||
assigneeContactId?: string;
|
||||
/** Thread parent — ID of the entry this is a reply to */
|
||||
threadParentId?: string;
|
||||
/** Linked entry IDs (for closing/archiving related entries) */
|
||||
linkedEntryIds: string[];
|
||||
/** File attachments */
|
||||
|
||||
Reference in New Issue
Block a user