Hot Desk 0.2.0: room layout proportions, name quick-select, remove notes

This commit is contained in:
AI Assistant
2026-03-08 19:33:25 +02:00
parent 4d2f924537
commit 94b342e5ce
5 changed files with 73 additions and 58 deletions
+5
View File
@@ -724,6 +724,7 @@ Env vars (hardcoded in docker-compose.yml for Portainer CE):
### 4B.04 `[STANDARD]` Registratura — Legal Deadline Workflow Fixes ### 4B.04 `[STANDARD]` Registratura — Legal Deadline Workflow Fixes
**What:** Fix gaps in the legal deadline tracking logic: **What:** Fix gaps in the legal deadline tracking logic:
- Chain deadline workflow (resolving one → prompt to add next in sequence) - Chain deadline workflow (resolving one → prompt to add next in sequence)
- Backward deadline edge cases (e.g., AC extension 45 working days BEFORE expiry) - Backward deadline edge cases (e.g., AC extension 45 working days BEFORE expiry)
- Tacit approval auto-detection when overdue + applicable type - Tacit approval auto-detection when overdue + applicable type
@@ -736,6 +737,7 @@ Env vars (hardcoded in docker-compose.yml for Portainer CE):
### 4B.05 `[STANDARD]` Tag Manager — Logic/Workflow Fix + ERP API ### 4B.05 `[STANDARD]` Tag Manager — Logic/Workflow Fix + ERP API
**What:** **What:**
- Fix tag assignment and filtering logic/workflow issues - Fix tag assignment and filtering logic/workflow issues
- Expose tags via API for external ERP integration (read-only endpoint for tag list + project assignments) - Expose tags via API for external ERP integration (read-only endpoint for tag list + project assignments)
- Verify ManicTime bidirectional sync still works - Verify ManicTime bidirectional sync still works
@@ -747,6 +749,7 @@ Env vars (hardcoded in docker-compose.yml for Portainer CE):
### 4B.06 `[STANDARD]` Prompt Generator — Bug Fixes + New Features ### 4B.06 `[STANDARD]` Prompt Generator — Bug Fixes + New Features
**What:** Address known bugs and implement new ideas (details TBD from user testing): **What:** Address known bugs and implement new ideas (details TBD from user testing):
- Fix any template rendering issues - Fix any template rendering issues
- Add new templates as requested - Add new templates as requested
- Implement user-suggested feature improvements - Implement user-suggested feature improvements
@@ -758,6 +761,7 @@ Env vars (hardcoded in docker-compose.yml for Portainer CE):
### 4B.07 `[HEAVY]` Authentik SSO — Verify & Fix ### 4B.07 `[HEAVY]` Authentik SSO — Verify & Fix
**What:** End-to-end verification that Authentik OIDC login works: **What:** End-to-end verification that Authentik OIDC login works:
- Verify auth.beletage.ro accessibility - Verify auth.beletage.ro accessibility
- Test login flow (redirect → auth → callback → session) - Test login flow (redirect → auth → callback → session)
- Verify group→role/company mapping - Verify group→role/company mapping
@@ -771,6 +775,7 @@ Env vars (hardcoded in docker-compose.yml for Portainer CE):
### 4B.08 `[STANDARD]` DB/Storage — End-to-End Verification ### 4B.08 `[STANDARD]` DB/Storage — End-to-End Verification
**What:** Verify all 14 modules correctly persist to PostgreSQL: **What:** Verify all 14 modules correctly persist to PostgreSQL:
- Test CRUD operations for each module - Test CRUD operations for each module
- Verify data survives container restart - Verify data survives container restart
- Check storage adapter fallback behavior - Check storage adapter fallback behavior
@@ -19,10 +19,10 @@ export function DeskRoomLayout({
return ( return (
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
{/* Room container — styled like a top-down floor plan */} {/* 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"> <div className="relative w-full max-w-[420px] rounded-xl border border-border/60 bg-muted/20 px-5 py-10">
{/* Window indicator — LEFT wall (landmark for orientation) */} {/* Window indicator — LEFT wall (landmark for orientation) */}
<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-[26.5%] bottom-[26.5%] 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"> <div className="absolute top-[28.5%] bottom-[28.5%] left-0 flex flex-col justify-between">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div <div
key={i} key={i}
@@ -30,21 +30,14 @@ export function DeskRoomLayout({
/> />
))} ))}
</div> </div>
{/* Window label */}
<div className="absolute left-1.5 top-1/2 -translate-y-1/2 -rotate-90 text-[9px] font-medium text-muted-foreground/40 tracking-widest uppercase select-none">
Fereastră
</div>
{/* Door indicator — RIGHT wall */} {/* Door indicator — RIGHT wall */}
<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-[63%] 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>
{/* Central table */} {/* Central table */}
<div className="mx-auto mt-4 mb-4 flex flex-col items-center"> <div className="mr-auto -ml-16 mt-4 mb-4 flex flex-col items-center">
{/* Top row desks */} {/* Top row desks */}
<div className="flex gap-3 mb-2"> <div className="flex gap-0 mb-0">
{DESKS.filter((d) => d.position.startsWith("top")).map((desk) => { {DESKS.filter((d) => d.position.startsWith("top")).map((desk) => {
const reservation = getReservationForDesk( const reservation = getReservationForDesk(
desk.id, desk.id,
@@ -65,10 +58,10 @@ export function DeskRoomLayout({
</div> </div>
{/* The table surface */} {/* The table surface */}
<div className="h-12 w-full max-w-[280px] rounded-md border border-border/50 bg-muted/40" /> <div className="h-3 w-[232px] rounded-sm border border-border/50 bg-muted/40" />
{/* Bottom row desks */} {/* Bottom row desks */}
<div className="flex gap-3 mt-2"> <div className="flex gap-0 mt-0">
{DESKS.filter((d) => d.position.startsWith("bottom")).map( {DESKS.filter((d) => d.position.startsWith("bottom")).map(
(desk) => { (desk) => {
const reservation = getReservationForDesk( const reservation = getReservationForDesk(
@@ -111,7 +104,7 @@ function DeskSlot({ label, reservation, side, onClick }: DeskSlotProps) {
type="button" type="button"
onClick={onClick} onClick={onClick}
className={cn( className={cn(
"group relative flex w-[125px] cursor-pointer flex-col items-center rounded-lg border p-3 transition-all", "group relative flex w-[116px] cursor-pointer flex-col items-center rounded-lg border p-3 transition-all",
side === "top" ? "rounded-b-sm" : "rounded-t-sm", side === "top" ? "rounded-b-sm" : "rounded-t-sm",
isBooked isBooked
? "border-primary/30 bg-primary/8 hover:border-primary/50 hover:bg-primary/12" ? "border-primary/30 bg-primary/8 hover:border-primary/50 hover:bg-primary/12"
@@ -199,9 +199,6 @@ export function HotDeskModule() {
<th className="px-3 py-1.5 text-left text-xs font-medium text-muted-foreground"> <th className="px-3 py-1.5 text-left text-xs font-medium text-muted-foreground">
Persoana Persoana
</th> </th>
<th className="px-3 py-1.5 text-left text-xs font-medium text-muted-foreground">
Note
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -216,9 +213,6 @@ export function HotDeskModule() {
r.deskId} r.deskId}
</td> </td>
<td className="px-3 py-1.5 text-xs">{r.userName}</td> <td className="px-3 py-1.5 text-xs">{r.userName}</td>
<td className="px-3 py-1.5 text-xs text-muted-foreground truncate max-w-[120px]">
{r.notes || "—"}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -11,11 +11,21 @@ import {
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input"; import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label"; import { Label } from "@/shared/components/ui/label";
import { Textarea } from "@/shared/components/ui/textarea"; import { useAuth } from "@/core/auth";
import { cn } from "@/shared/lib/utils";
import type { DeskId, DeskReservation } from "../types"; import type { DeskId, DeskReservation } from "../types";
import { getDeskLabel } from "../types"; import { getDeskLabel } from "../types";
import { formatDateRo, isDateBookable } from "../services/reservation-service"; import { formatDateRo, isDateBookable } from "../services/reservation-service";
const TEAM_NAMES = [
"Alexandra",
"Mihaela",
"Ștefan",
"Mara",
"Denisa",
"Daria",
];
interface ReservationDialogProps { interface ReservationDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -36,8 +46,22 @@ export function ReservationDialog({
onCancel, onCancel,
}: ReservationDialogProps) { }: ReservationDialogProps) {
const [userName, setUserName] = useState(""); const [userName, setUserName] = useState("");
const [notes, setNotes] = useState("");
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const { user } = useAuth();
// Auto-fill first name from Authentik session
useEffect(() => {
if (open && !userName && user?.name) {
const firstName = user.name.split(" ")[0];
if (firstName) setUserName(firstName);
}
}, [open, user?.name]); // eslint-disable-line react-hooks/exhaustive-deps
const filteredNames = userName.trim()
? TEAM_NAMES.filter((n) =>
n.toLowerCase().startsWith(userName.toLowerCase()),
)
: TEAM_NAMES;
const bookable = isDateBookable(dateKey); const bookable = isDateBookable(dateKey);
const isBooked = !!existingReservation; const isBooked = !!existingReservation;
@@ -46,9 +70,8 @@ export function ReservationDialog({
if (!userName.trim()) return; if (!userName.trim()) return;
setSubmitting(true); setSubmitting(true);
try { try {
await onBook(userName.trim(), notes.trim()); await onBook(userName.trim(), "");
setUserName(""); setUserName("");
setNotes("");
onClose(); onClose();
} finally { } finally {
setSubmitting(false); setSubmitting(false);
@@ -84,12 +107,6 @@ export function ReservationDialog({
{existingReservation.userName} {existingReservation.userName}
</span> </span>
</div> </div>
{existingReservation.notes && (
<div className="text-sm">
<span className="text-muted-foreground">Note: </span>
<span>{existingReservation.notes}</span>
</div>
)}
</div> </div>
</div> </div>
) : bookable ? ( ) : bookable ? (
@@ -104,17 +121,23 @@ export function ReservationDialog({
onKeyDown={(e) => e.key === "Enter" && handleBook()} onKeyDown={(e) => e.key === "Enter" && handleBook()}
autoFocus autoFocus
/> />
<div className="flex flex-wrap gap-1.5">
{filteredNames.map((name) => (
<button
key={name}
type="button"
onClick={() => setUserName(name)}
className={cn(
"rounded-md border px-2.5 py-1 text-xs transition-colors",
userName === name
? "border-primary bg-primary/10 text-primary font-medium"
: "border-border/50 bg-muted/30 text-muted-foreground hover:border-primary/40 hover:bg-muted/50",
)}
>
{name}
</button>
))}
</div> </div>
<div className="space-y-2">
<Label htmlFor="notes">Note (opțional)</Label>
<Textarea
id="notes"
placeholder="Ex: lucrez la proiect X, am nevoie de monitor extern..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={2}
className="resize-none"
/>
</div> </div>
</div> </div>
) : ( ) : (