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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user