pre-launch hardening: Address Book type sort, Hot Desk proportions, TVA calculator, ROADMAP Phase 4B

- Address Book: type dropdown always sorted alphabetically (ro locale), including custom types
- Hot Desk: window ~half height (top-[35%] bottom-[35%]), door ~double height (h-16)
- Mini Utilities: TVA calculator (19%) with add/extract modes, RON formatting, copy buttons
- ROADMAP: new Phase 4B Pre-Launch Hardening with 10 structured tasks
- CLAUDE.md: bumped versions (Address Book 0.1.1, Mini Utilities 0.1.1, Hot Desk 0.1.1), Visual Copilot separate repo note
This commit is contained in:
AI Assistant
2026-03-08 14:08:48 +02:00
parent a6fa94deec
commit 4d2f924537
6 changed files with 269 additions and 47 deletions
@@ -80,15 +80,22 @@ export function AddressBookModule() {
null,
);
// Collect all contact types (defaults + custom ones from existing contacts)
// Collect all contact types (defaults + custom ones from existing contacts), sorted alphabetically by label
const allTypes = useMemo(() => {
const types = { ...DEFAULT_TYPE_LABELS };
const types: Record<string, string> = { ...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;
// Sort entries alphabetically by label
const sorted: Record<string, string> = {};
for (const [k, v] of Object.entries(types).sort((a, b) =>
a[1].localeCompare(b[1], "ro"),
)) {
sorted[k] = v;
}
return sorted;
}, [allContacts]);
const handleSubmit = async (
@@ -652,15 +659,16 @@ function CreatableTypeSelect({
<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>
)}
{Object.entries(DEFAULT_TYPE_LABELS)
.concat(
value && !DEFAULT_TYPE_LABELS[value] ? [[value, value]] : [],
)
.sort((a, b) => a[1].localeCompare(b[1], "ro"))
.map(([k, label]) => (
<SelectItem key={k} value={k}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
@@ -21,9 +21,9 @@ export function DeskRoomLayout({
{/* Room container — styled like a top-down floor plan */}
<div className="relative w-full max-w-[340px] rounded-xl border border-border/60 bg-muted/20 p-5">
{/* Window indicator — LEFT wall (landmark for orientation) */}
<div className="absolute top-4 bottom-4 left-0 w-1.5 rounded-r-sm bg-sky-300/30 dark:bg-sky-500/20" />
<div className="absolute top-6 bottom-6 left-0 flex flex-col justify-between">
{Array.from({ length: 6 }).map((_, i) => (
<div className="absolute top-[35%] bottom-[35%] left-0 w-1.5 rounded-r-sm bg-sky-300/30 dark:bg-sky-500/20" />
<div className="absolute top-[37%] bottom-[37%] left-0 flex flex-col justify-between">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="ml-0.5 h-3 w-0.5 rounded-full bg-sky-400/25 dark:bg-sky-400/15"
@@ -36,8 +36,8 @@ export function DeskRoomLayout({
</div>
{/* Door indicator — RIGHT wall */}
<div className="absolute top-[60%] right-0 h-8 w-1.5 rounded-l-sm bg-amber-400/25 dark:bg-amber-500/15" />
<div className="absolute top-[60%] right-1.5 translate-y-1 text-[8px] text-muted-foreground/30 select-none">
<div className="absolute top-[55%] right-0 h-16 w-1.5 rounded-l-sm bg-amber-400/25 dark:bg-amber-500/15" />
<div className="absolute top-[55%] right-1.5 translate-y-1 text-[8px] text-muted-foreground/30 select-none">
Ușă
</div>
@@ -16,6 +16,7 @@ import {
CaseUpper,
Palette,
Upload,
Receipt,
} from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input";
@@ -206,6 +207,103 @@ function PercentageCalculator() {
);
}
function TvaCalculator() {
const TVA_RATE = 19; // Romania standard VAT
const [amount, setAmount] = useState("");
const [mode, setMode] = useState<"add" | "extract">("add");
const val = parseFloat(amount);
const tvaMultiplier = TVA_RATE / 100;
const cuTva = !isNaN(val) && mode === "add" ? val * (1 + tvaMultiplier) : NaN;
const faraTva =
!isNaN(val) && mode === "extract" ? val / (1 + tvaMultiplier) : NaN;
const tvaAmount = !isNaN(val)
? mode === "add"
? val * tvaMultiplier
: val - val / (1 + tvaMultiplier)
: NaN;
const fmt = (n: number) =>
isNaN(n)
? "—"
: n.toLocaleString("ro-RO", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button
variant={mode === "add" ? "default" : "outline"}
size="sm"
onClick={() => setMode("add")}
>
Adaugă TVA
</Button>
<Button
variant={mode === "extract" ? "default" : "outline"}
size="sm"
onClick={() => setMode("extract")}
>
Extrage TVA
</Button>
</div>
<div>
<Label>{mode === "add" ? "Sumă fără TVA" : "Sumă cu TVA"}</Label>
<div className="mt-1 flex gap-2">
<Input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder={mode === "add" ? "Ex: 1000" : "Ex: 1190"}
className="flex-1"
/>
<span className="flex items-center text-sm text-muted-foreground">
RON
</span>
</div>
</div>
{!isNaN(val) && val > 0 && (
<div className="rounded-md border bg-muted/30 p-4 space-y-2 text-sm">
{mode === "add" ? (
<>
<p>
Sumă fără TVA: <strong>{fmt(val)} RON</strong>
</p>
<p>
TVA ({TVA_RATE}%): <strong>{fmt(tvaAmount)} RON</strong>
<CopyButton text={fmt(tvaAmount)} />
</p>
<p className="text-base pt-1 border-t">
Total cu TVA:{" "}
<strong className="text-primary">{fmt(cuTva)} RON</strong>
<CopyButton text={fmt(cuTva)} />
</p>
</>
) : (
<>
<p>
Sumă cu TVA: <strong>{fmt(val)} RON</strong>
</p>
<p>
TVA ({TVA_RATE}%): <strong>{fmt(tvaAmount)} RON</strong>
<CopyButton text={fmt(tvaAmount)} />
</p>
<p className="text-base pt-1 border-t">
Sumă fără TVA:{" "}
<strong className="text-primary">{fmt(faraTva)} RON</strong>
<CopyButton text={fmt(faraTva)} />
</p>
</>
)}
</div>
)}
</div>
);
}
function AreaConverter() {
const units = [
{ key: "mp", label: "mp (m²)", factor: 1 },
@@ -1207,6 +1305,9 @@ export function MiniUtilitiesModule() {
<TabsTrigger value="percentage">
<Percent className="mr-1 h-3.5 w-3.5" /> Procente
</TabsTrigger>
<TabsTrigger value="tva">
<Receipt className="mr-1 h-3.5 w-3.5" /> TVA
</TabsTrigger>
<TabsTrigger value="area">
<Ruler className="mr-1 h-3.5 w-3.5" /> Suprafețe
</TabsTrigger>
@@ -1263,6 +1364,16 @@ export function MiniUtilitiesModule() {
</CardContent>
</Card>
</TabsContent>
<TabsContent value="tva">
<Card>
<CardHeader>
<CardTitle className="text-base">Calculator TVA (19%)</CardTitle>
</CardHeader>
<CardContent>
<TvaCalculator />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="area">
<Card>
<CardHeader>