feat: 3.01 header logos+nav, 3.09 dynamic contact types, 3.10 hot-desk window, 3.11 vault email+link

This commit is contained in:
AI Assistant
2026-02-27 11:33:20 +02:00
parent 100e52222e
commit 042e4e1108
6 changed files with 242 additions and 57 deletions
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useMemo } from "react";
import {
Plus,
Pencil,
@@ -46,7 +46,7 @@ import { useTags } from "@/core/tagging";
import { downloadVCard } from "../services/vcard-export";
import { useRegistry } from "@/modules/registratura/hooks/use-registry";
const TYPE_LABELS: Record<ContactType, string> = {
const DEFAULT_TYPE_LABELS: Record<string, string> = {
client: "Client",
supplier: "Furnizor",
institution: "Instituție",
@@ -54,6 +54,11 @@ const TYPE_LABELS: Record<ContactType, string> = {
internal: "Intern",
};
/** Get a label for any contact type, including custom ones */
function getTypeLabel(type: string): string {
return DEFAULT_TYPE_LABELS[type] ?? type;
}
type ViewMode = "list" | "add" | "edit";
export function AddressBookModule() {
@@ -75,6 +80,17 @@ export function AddressBookModule() {
null,
);
// Collect all contact types (defaults + custom ones from existing contacts)
const allTypes = useMemo(() => {
const types = { ...DEFAULT_TYPE_LABELS };
for (const c of allContacts) {
if (c.type && !types[c.type]) {
types[c.type] = c.type; // custom type — label is the type itself
}
}
return types;
}, [allContacts]);
const handleSubmit = async (
data: Omit<AddressContact, "id" | "createdAt" | "updatedAt">,
) => {
@@ -97,18 +113,20 @@ export function AddressBookModule() {
<p className="text-2xl font-bold">{allContacts.length}</p>
</CardContent>
</Card>
{(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 4).map((type) => (
<Card key={type}>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">
{TYPE_LABELS[type]}
</p>
<p className="text-2xl font-bold">
{allContacts.filter((c) => c.type === type).length}
</p>
</CardContent>
</Card>
))}
{Object.keys(DEFAULT_TYPE_LABELS)
.slice(0, 4)
.map((type) => (
<Card key={type}>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">
{getTypeLabel(type)}
</p>
<p className="text-2xl font-bold">
{allContacts.filter((c) => c.type === type).length}
</p>
</CardContent>
</Card>
))}
</div>
{viewMode === "list" && (
@@ -134,9 +152,9 @@ export function AddressBookModule() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate tipurile</SelectItem>
{(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => (
{Object.keys(allTypes).map((t) => (
<SelectItem key={t} value={t}>
{TYPE_LABELS[t]}
{allTypes[t]}
</SelectItem>
))}
</SelectContent>
@@ -269,7 +287,7 @@ function ContactCard({
</p>
)}
<Badge variant="outline" className="text-[10px]">
{TYPE_LABELS[contact.type]}
{getTypeLabel(contact.type)}
</Badge>
{contact.department && (
<Badge variant="secondary" className="text-[10px]">
@@ -380,7 +398,7 @@ function ContactDetailDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-3">
<span>{contact.name}</span>
<Badge variant="outline">{TYPE_LABELS[contact.type]}</Badge>
<Badge variant="outline">{getTypeLabel(contact.type)}</Badge>
</DialogTitle>
</DialogHeader>
@@ -565,6 +583,99 @@ function ContactDetailDialog({
);
}
// ── Creatable Type Select ──
function CreatableTypeSelect({
value,
onChange,
}: {
value: string;
onChange: (v: string) => void;
}) {
const [isCreating, setIsCreating] = useState(false);
const [customValue, setCustomValue] = useState("");
if (isCreating) {
return (
<div className="mt-1 flex gap-1.5">
<Input
value={customValue}
onChange={(e) => setCustomValue(e.target.value)}
placeholder="Tip nou (ex: Colaborator Extern)"
className="flex-1"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && customValue.trim()) {
e.preventDefault();
onChange(customValue.trim());
setIsCreating(false);
setCustomValue("");
}
if (e.key === "Escape") {
setIsCreating(false);
setCustomValue("");
}
}}
/>
<Button
type="button"
size="sm"
onClick={() => {
if (customValue.trim()) {
onChange(customValue.trim());
}
setIsCreating(false);
setCustomValue("");
}}
>
OK
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setIsCreating(false);
setCustomValue("");
}}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
);
}
return (
<div className="mt-1 flex gap-1.5">
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(DEFAULT_TYPE_LABELS).map(([k, label]) => (
<SelectItem key={k} value={k}>
{label}
</SelectItem>
))}
{/* Show current custom value if not in defaults */}
{value && !DEFAULT_TYPE_LABELS[value] && (
<SelectItem value={value}>{value}</SelectItem>
)}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setIsCreating(true)}
title="Adaugă tip nou"
>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
);
}
// ── Contact Form ──
function ContactForm({
@@ -675,18 +786,7 @@ function ContactForm({
</div>
<div>
<Label>Tip</Label>
<Select value={type} onValueChange={(v) => setType(v as ContactType)}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => (
<SelectItem key={t} value={t}>
{TYPE_LABELS[t]}
</SelectItem>
))}
</SelectContent>
</Select>
<CreatableTypeSelect value={type} onChange={(v) => setType(v)} />
</div>
</div>
+8 -2
View File
@@ -1,6 +1,12 @@
import type { Visibility } from '@/core/module-registry/types';
import type { Visibility } from "@/core/module-registry/types";
export type ContactType = 'client' | 'supplier' | 'institution' | 'collaborator' | 'internal';
export type ContactType =
| "client"
| "supplier"
| "institution"
| "collaborator"
| "internal"
| string;
/** A contact person within an organization/entity */
export interface ContactPerson {