diff --git a/src/modules/address-book/components/address-book-module.tsx b/src/modules/address-book/components/address-book-module.tsx index 8b377f9..b4dbbba 100644 --- a/src/modules/address-book/components/address-book-module.tsx +++ b/src/modules/address-book/components/address-book-module.tsx @@ -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 = { +const DEFAULT_TYPE_LABELS: Record = { client: "Client", supplier: "Furnizor", institution: "Instituție", @@ -54,6 +54,11 @@ const TYPE_LABELS: Record = { 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, ) => { @@ -97,18 +113,20 @@ export function AddressBookModule() {

{allContacts.length}

- {(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 4).map((type) => ( - - -

- {TYPE_LABELS[type]} -

-

- {allContacts.filter((c) => c.type === type).length} -

-
-
- ))} + {Object.keys(DEFAULT_TYPE_LABELS) + .slice(0, 4) + .map((type) => ( + + +

+ {getTypeLabel(type)} +

+

+ {allContacts.filter((c) => c.type === type).length} +

+
+
+ ))} {viewMode === "list" && ( @@ -134,9 +152,9 @@ export function AddressBookModule() { Toate tipurile - {(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => ( + {Object.keys(allTypes).map((t) => ( - {TYPE_LABELS[t]} + {allTypes[t]} ))} @@ -269,7 +287,7 @@ function ContactCard({

)} - {TYPE_LABELS[contact.type]} + {getTypeLabel(contact.type)} {contact.department && ( @@ -380,7 +398,7 @@ function ContactDetailDialog({ {contact.name} - {TYPE_LABELS[contact.type]} + {getTypeLabel(contact.type)} @@ -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 ( +
+ 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(""); + } + }} + /> + + +
+ ); + } + + return ( +
+ + +
+ ); +} + // ── Contact Form ── function ContactForm({ @@ -675,18 +786,7 @@ function ContactForm({
- + setType(v)} />
diff --git a/src/modules/address-book/types.ts b/src/modules/address-book/types.ts index 606f0f3..9d0fd4b 100644 --- a/src/modules/address-book/types.ts +++ b/src/modules/address-book/types.ts @@ -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 { diff --git a/src/modules/hot-desk/components/desk-room-layout.tsx b/src/modules/hot-desk/components/desk-room-layout.tsx index 8bd208d..608caaf 100644 --- a/src/modules/hot-desk/components/desk-room-layout.tsx +++ b/src/modules/hot-desk/components/desk-room-layout.tsx @@ -20,16 +20,26 @@ export function DeskRoomLayout({
{/* Room container — styled like a top-down floor plan */}
- {/* Window indicator — top edge */} -
-
- {Array.from({ length: 8 }).map((_, i) => ( + {/* Window indicator — LEFT wall (landmark for orientation) */} +
+
+ {Array.from({ length: 6 }).map((_, i) => (
))}
+ {/* Window label */} +
+ Fereastră +
+ + {/* Door indicator — RIGHT wall */} +
+
+ Ușă +
{/* Central table */}
diff --git a/src/modules/password-vault/components/password-vault-module.tsx b/src/modules/password-vault/components/password-vault-module.tsx index e9eaf75..1183a3f 100644 --- a/src/modules/password-vault/components/password-vault-module.tsx +++ b/src/modules/password-vault/components/password-vault-module.tsx @@ -267,6 +267,11 @@ export function PasswordVaultModule() {

{entry.username} + {entry.email && ( + + ({entry.email}) + + )}

@@ -301,9 +306,19 @@ export function PasswordVaultModule() { )}
{entry.url && ( -

+ e.stopPropagation()} + > {entry.url} -

+ )} {entry.customFields && entry.customFields.length > 0 && (
@@ -408,6 +423,7 @@ function VaultForm({ }) { const [label, setLabel] = useState(initial?.label ?? ""); const [username, setUsername] = useState(initial?.username ?? ""); + const [email, setEmail] = useState(initial?.email ?? ""); const [password, setPassword] = useState(initial?.password ?? ""); const [url, setUrl] = useState(initial?.url ?? ""); const [category, setCategory] = useState( @@ -468,6 +484,7 @@ function VaultForm({ onSubmit({ label, username, + email, password, url, category, @@ -539,6 +556,16 @@ function VaultForm({ />
+
+ + setEmail(e.target.value)} + className="mt-1" + placeholder="utilizator@exemplu.ro" + /> +
diff --git a/src/modules/password-vault/types.ts b/src/modules/password-vault/types.ts index 62c74e4..200ec48 100644 --- a/src/modules/password-vault/types.ts +++ b/src/modules/password-vault/types.ts @@ -19,6 +19,7 @@ export interface VaultEntry { id: string; label: string; username: string; + email: string; password: string; url: string; category: VaultEntryCategory; diff --git a/src/shared/components/layout/sidebar.tsx b/src/shared/components/layout/sidebar.tsx index 5ae18bc..b5df533 100644 --- a/src/shared/components/layout/sidebar.tsx +++ b/src/shared/components/layout/sidebar.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useMemo } from "react"; +import { useMemo, useState, useCallback, useEffect } from "react"; import * as Icons from "lucide-react"; import { buildNavigation } from "@/config/navigation"; import { COMPANIES } from "@/config/companies"; @@ -64,24 +64,61 @@ function NavItem({ ); } -function SidebarLogo() { - const sdt = COMPANIES["studii-de-teren"]; +const LOGO_COMPANIES = ["studii-de-teren", "urban-switch"] as const; - const logoSrc = sdt.logo?.light ?? null; +function SidebarLogos() { + const [clickCount, setClickCount] = useState(0); + const [shuffled, setShuffled] = useState(false); - if (!logoSrc) { - return ; - } + const handleLogoClick = useCallback(() => { + setClickCount((prev) => { + const next = prev + 1; + if (next >= 3) { + setShuffled((s) => !s); + return 0; + } + return next; + }); + }, []); + + // Reset click count after 2 seconds of inactivity + useEffect(() => { + if (clickCount === 0) return; + const timer = setTimeout(() => setClickCount(0), 2000); + return () => clearTimeout(timer); + }, [clickCount]); + + const logos = shuffled ? [...LOGO_COMPANIES].reverse() : [...LOGO_COMPANIES]; return ( - {sdt.shortName} +
+ {logos.map((companyId) => { + const company = COMPANIES[companyId]; + const logoSrc = company?.logo?.light; + if (!logoSrc) return null; + return ( + + ); + })} +
); } @@ -91,10 +128,14 @@ export function Sidebar() { return (