diff --git a/src/modules/mini-utilities/components/mini-utilities-module.tsx b/src/modules/mini-utilities/components/mini-utilities-module.tsx index 9aeb235..b5cc2d7 100644 --- a/src/modules/mini-utilities/components/mini-utilities-module.tsx +++ b/src/modules/mini-utilities/components/mini-utilities-module.tsx @@ -22,6 +22,10 @@ import { ArrowDown, Unlock, PenTool, + Maximize2, + LayoutGrid, + Plus, + X, } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; @@ -212,13 +216,18 @@ function PercentageCalculator() { ); } +const TVA_PRESETS = [5, 9, 19, 21]; + function TvaCalculator() { - const TVA_RATE = 19; // Romania standard VAT + const [tvaRate, setTvaRate] = useState(19); + const [customRate, setCustomRate] = useState(""); const [amount, setAmount] = useState(""); const [mode, setMode] = useState<"add" | "extract">("add"); + const effectiveRate = + customRate !== "" ? parseFloat(customRate) || tvaRate : tvaRate; const val = parseFloat(amount); - const tvaMultiplier = TVA_RATE / 100; + const tvaMultiplier = effectiveRate / 100; const cuTva = !isNaN(val) && mode === "add" ? val * (1 + tvaMultiplier) : NaN; const faraTva = @@ -239,6 +248,38 @@ function TvaCalculator() { return (
+ {/* Rate selector */} +
+ +
+ {TVA_PRESETS.map((r) => ( + + ))} +
+ setCustomRate(e.target.value)} + placeholder="Altă cotă" + className="w-28 text-sm" + /> + {customRate && ( + % + )} +
+
+
+ ))} +
+ 1: + setCustomScale(e.target.value)} + placeholder="Altă scară" + className="w-28 text-sm" + /> +
+
+
+
+ + +
+
+ +
+ setInputVal(e.target.value)} + placeholder={mode === "real-to-drawing" ? "ex: 5.4" : "ex: 54"} + className="flex-1" + /> + + {unitIn} + +
+
+ {!isNaN(val) && val > 0 && ( +
+

+ Scară 1:{effectiveScale} +

+

+ {mode === "real-to-drawing" ? ( + <> + {val} m real →{" "} + {fmtDesen(result)} pe desen + + + ) : ( + <> + {val} mm desen →{" "} + {fmtReal(result)} real + + + )} +

+ {mode === "real-to-drawing" && !isNaN(result) && ( +

+ = {(result / 10).toFixed(2)} cm pe desen +

+ )} +
+ )} + + ); +} + +// ─── Calculator Camere ──────────────────────────────────────────────────────── + +interface Room { + id: string; + name: string; + width: string; + length: string; + height: string; +} + +function RoomAreaCalculator() { + const [rooms, setRooms] = useState([ + { id: "1", name: "Living", width: "", length: "", height: "" }, + ]); + + const addRoom = () => { + setRooms((prev) => [ + ...prev, + { id: String(Date.now()), name: "", width: "", length: "", height: "" }, + ]); + }; + + const removeRoom = (id: string) => { + setRooms((prev) => prev.filter((r) => r.id !== id)); + }; + + const updateRoom = (id: string, field: keyof Room, value: string) => { + setRooms((prev) => + prev.map((r) => (r.id === id ? { ...r, [field]: value } : r)), + ); + }; + + const roomsWithArea = rooms.map((r) => { + const w = parseFloat(r.width); + const l = parseFloat(r.length); + const h = parseFloat(r.height); + const area = !isNaN(w) && !isNaN(l) ? w * l : NaN; + const volume = !isNaN(area) && !isNaN(h) ? area * h : NaN; + return { ...r, area, volume }; + }); + + const totalArea = roomsWithArea.reduce( + (sum, r) => (isNaN(r.area) ? sum : sum + r.area), + 0, + ); + const totalVolume = roomsWithArea.reduce( + (sum, r) => (isNaN(r.volume) ? sum : sum + r.volume), + 0, + ); + + const fmtM2 = (n: number) => + n.toLocaleString("ro-RO", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + + const copyAll = () => { + const lines = roomsWithArea + .filter((r) => !isNaN(r.area)) + .map((r) => `${r.name || "Cameră"}: ${fmtM2(r.area)} m²`) + .join("\n"); + const text = lines + `\n\nTotal: ${fmtM2(totalArea)} m²`; + navigator.clipboard.writeText(text).catch(() => {}); + }; + + return ( +
+
+ {roomsWithArea.map((room, idx) => ( +
+ updateRoom(room.id, "name", e.target.value)} + className="w-[130px] text-sm" + /> +
+ updateRoom(room.id, "width", e.target.value)} + className="w-[80px] text-sm" + min={0} + step={0.01} + /> + × + updateRoom(room.id, "length", e.target.value)} + className="w-[80px] text-sm" + min={0} + step={0.01} + /> + m +
+
+ updateRoom(room.id, "height", e.target.value)} + className="w-[70px] text-sm" + min={0} + step={0.01} + /> + m +
+ {!isNaN(room.area) && ( + + {fmtM2(room.area)} m² + {!isNaN(room.volume) && ( + + {fmtM2(room.volume)} m³ + + )} + + )} + +
+ ))} +
+
+ +
+ {totalArea > 0 && ( +
+
+ + Total suprafață:{" "} + {fmtM2(totalArea)} m² + + +
+ {totalVolume > 0 && ( +

+ Volum total: {fmtM2(totalVolume)} m³ +

+ )} + +
+ )} +
+ ); +} + // ─── Main Module ────────────────────────────────────────────────────────────── export function MiniUtilitiesModule() { @@ -2521,6 +2862,12 @@ export function MiniUtilitiesModule() { MDLPA + + Scară + + + Camere + {/* ── Documente & Unelte ── */} @@ -2578,7 +2925,7 @@ export function MiniUtilitiesModule() { - Calculator TVA (19%) + Calculator TVA @@ -2705,6 +3052,26 @@ export function MiniUtilitiesModule() { + + + + Calculator scară desen + + + + + + + + + + Calculator suprafețe camere + + + + + + ); } diff --git a/src/modules/password-vault/components/password-vault-module.tsx b/src/modules/password-vault/components/password-vault-module.tsx index 7396d30..e2ceb9f 100644 --- a/src/modules/password-vault/components/password-vault-module.tsx +++ b/src/modules/password-vault/components/password-vault-module.tsx @@ -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() { ))} )} + {entry.additionalUsers && + entry.additionalUsers.length > 0 && ( + + + +{entry.additionalUsers.length} utilizator + {entry.additionalUsers.length > 1 ? "i" : ""} + + )}
{/* WiFi QR button */} @@ -695,6 +710,12 @@ function VaultForm({ const [customFields, setCustomFields] = useState( initial?.customFields ?? [], ); + const [additionalUsers, setAdditionalUsers] = useState( + 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({ )}
+ {/* Additional users */} +
+ + {showAdditionalUsers && ( +
+

+ Adaugă credențiale suplimentare pentru același cont (ex: mai mulți angajați cu acces). +

+ {additionalUsers.map((u, i) => ( +
+ +

+ Utilizator {i + 1} +

+
+
+ + + 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" + /> +
+
+ + + 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" + /> +
+
+
+ + + 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ă" + /> +
+
+ + + 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" + /> +
+
+ ))} + +
+ )} +
+