feat(utilities+vault): TVA configurable rate, scale/rooms calculators, multi-user vault

Mini Utilities:
- TVA calculator: rate now configurable (5%/9%/19%/21% presets + custom input)
  replaces hardcoded 19% constant; displays effective rate in results
- New tab: Calculator scară desen — real↔drawing conversion with 6 scale presets
  (1:50..1:5000) + custom, shows cm equivalent for drawing→real
- New tab: Calculator suprafețe camere — multi-room area accumulator
  (name + W×L×H), live m²/m³ per room, running total, copy-all

Password Vault:
- New VaultUser type: { username, password, email?, notes? }
- VaultEntry.additionalUsers: VaultUser[] — backward compat (defaults to [])
- VaultForm: collapsible "Utilizatori suplimentari" section, add/remove rows
- Card list: badge showing count of additional users when present
- useVault: normalizes legacy entries (additionalUsers ?? [])

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-12 20:51:34 +02:00
parent b0f27053ae
commit 06b3a820de
4 changed files with 531 additions and 7 deletions
@@ -22,6 +22,10 @@ import {
HardDrive,
MoreHorizontal,
QrCode,
Users,
UserPlus,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input";
@@ -50,7 +54,7 @@ import {
} from "@/shared/components/ui/dialog";
import { Switch } from "@/shared/components/ui/switch";
import type { CompanyId } from "@/core/auth/types";
import type { VaultEntry, VaultEntryCategory, CustomField } from "../types";
import type { VaultEntry, VaultEntryCategory, CustomField, VaultUser } from "../types";
import { useVault } from "../hooks/use-vault";
// Category definitions with icons
@@ -429,6 +433,17 @@ export function PasswordVaultModule() {
))}
</div>
)}
{entry.additionalUsers &&
entry.additionalUsers.length > 0 && (
<Badge
variant="outline"
className="text-[10px] gap-1"
>
<Users className="h-2.5 w-2.5" />
+{entry.additionalUsers.length} utilizator
{entry.additionalUsers.length > 1 ? "i" : ""}
</Badge>
)}
</div>
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
{/* WiFi QR button */}
@@ -695,6 +710,12 @@ function VaultForm({
const [customFields, setCustomFields] = useState<CustomField[]>(
initial?.customFields ?? [],
);
const [additionalUsers, setAdditionalUsers] = useState<VaultUser[]>(
initial?.additionalUsers ?? [],
);
const [showAdditionalUsers, setShowAdditionalUsers] = useState(
(initial?.additionalUsers?.length ?? 0) > 0,
);
// Password generator state
const [genLength, setGenLength] = useState(16);
@@ -750,6 +771,7 @@ function VaultForm({
company,
notes,
customFields: customFields.filter((cf) => cf.key.trim()),
additionalUsers: additionalUsers.filter((u) => u.username.trim() || u.password.trim()),
tags: initial?.tags ?? [],
visibility: initial?.visibility ?? "admin",
});
@@ -1024,6 +1046,127 @@ function VaultForm({
)}
</div>
{/* Additional users */}
<div>
<button
type="button"
className="flex items-center gap-1.5 text-sm font-medium hover:text-primary transition-colors"
onClick={() => setShowAdditionalUsers((v) => !v)}
>
<Users className="h-3.5 w-3.5" />
Utilizatori suplimentari
{additionalUsers.length > 0 && (
<Badge variant="secondary" className="text-[10px] ml-1">
{additionalUsers.length}
</Badge>
)}
{showAdditionalUsers ? (
<ChevronUp className="h-3.5 w-3.5 ml-auto" />
) : (
<ChevronDown className="h-3.5 w-3.5 ml-auto" />
)}
</button>
{showAdditionalUsers && (
<div className="mt-2 space-y-2 rounded border p-3">
<p className="text-[11px] text-muted-foreground">
Adaugă credențiale suplimentare pentru același cont (ex: mai mulți angajați cu acces).
</p>
{additionalUsers.map((u, i) => (
<div key={i} className="space-y-1.5 rounded-sm border p-2 relative">
<button
type="button"
className="absolute right-1 top-1 text-destructive hover:opacity-80"
onClick={() =>
setAdditionalUsers((prev) => prev.filter((_, idx) => idx !== i))
}
>
<X className="h-3.5 w-3.5" />
</button>
<p className="text-xs font-medium text-muted-foreground">
Utilizator {i + 1}
</p>
<div className="grid gap-1.5 sm:grid-cols-2">
<div>
<Label className="text-xs">Utilizator / Titular</Label>
<Input
value={u.username}
onChange={(e) =>
setAdditionalUsers((prev) =>
prev.map((x, idx) =>
idx === i ? { ...x, username: e.target.value } : x,
),
)
}
className="mt-0.5 text-sm h-8"
placeholder="nume_utilizator"
/>
</div>
<div>
<Label className="text-xs">Email (opțional)</Label>
<Input
type="email"
value={u.email ?? ""}
onChange={(e) =>
setAdditionalUsers((prev) =>
prev.map((x, idx) =>
idx === i ? { ...x, email: e.target.value } : x,
),
)
}
className="mt-0.5 text-sm h-8"
placeholder="email@exemplu.ro"
/>
</div>
</div>
<div>
<Label className="text-xs">Parolă</Label>
<Input
value={u.password}
onChange={(e) =>
setAdditionalUsers((prev) =>
prev.map((x, idx) =>
idx === i ? { ...x, password: e.target.value } : x,
),
)
}
className="mt-0.5 text-sm font-mono h-8"
placeholder="parolă"
/>
</div>
<div>
<Label className="text-xs">Note (opțional)</Label>
<Input
value={u.notes ?? ""}
onChange={(e) =>
setAdditionalUsers((prev) =>
prev.map((x, idx) =>
idx === i ? { ...x, notes: e.target.value } : x,
),
)
}
className="mt-0.5 text-sm h-8"
placeholder="ex: contul lui Mihai"
/>
</div>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
setAdditionalUsers((prev) => [
...prev,
{ username: "", password: "", email: "", notes: "" },
])
}
>
<UserPlus className="mr-1 h-3 w-3" /> Adaugă utilizator
</Button>
</div>
)}
</div>
<div>
<Label>Note</Label>
<Textarea
@@ -26,7 +26,11 @@ export function useVault() {
try {
const res = await fetch("/api/vault");
const data = await res.json();
const results = (data.entries ?? []) as VaultEntry[];
const results = ((data.entries ?? []) as VaultEntry[]).map((e) => ({
...e,
additionalUsers: e.additionalUsers ?? [],
customFields: e.customFields ?? [],
}));
results.sort((a, b) => a.label.localeCompare(b.label));
setEntries(results);
} catch (err) {
+10
View File
@@ -18,6 +18,14 @@ export interface CustomField {
value: string;
}
/** Additional user credential for multi-user entries */
export interface VaultUser {
username: string;
password: string;
email?: string;
notes?: string;
}
export interface VaultEntry {
id: string;
label: string;
@@ -29,6 +37,8 @@ export interface VaultEntry {
company: CompanyId;
/** Custom key-value fields */
customFields: CustomField[];
/** Additional users/credentials for shared accounts */
additionalUsers: VaultUser[];
notes: string;
tags: string[];
visibility: Visibility;