chore: subir version inicial del Tablero CDC

This commit is contained in:
Isaac Aracena
2026-05-23 11:58:41 -04:00
commit 36fdf98419
77 changed files with 15092 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
import React, { useMemo, useState } from "react";
import { Plus, Search, LayoutGrid, ChevronDown, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { ProjectCard } from "@/components/board/ProjectCard";
import { ProjectDialog } from "@/components/board/ProjectDialog";
import { useProjects, type Project } from "@/lib/store";
import { useAuth } from "@/context/AuthContext";
/** Extract up-to-2-letter initials from a display name or email */
function initials(name: string | null | undefined, email: string | null | undefined): string {
if (name) {
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
return name.slice(0, 2).toUpperCase();
}
if (email) return email.slice(0, 2).toUpperCase();
return "?";
}
export default function BoardPage() {
const { projects } = useProjects();
const { user, isGerardo, logout } = useAuth();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<Project | null>(null);
const [query, setQuery] = useState("");
const [filter, setFilter] = useState<"todos" | "abiertos" | "cerrados">("todos");
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
return projects.filter((p) => {
if (filter === "abiertos" && p.status) return false;
if (filter === "cerrados" && !p.status) return false;
if (!q) return true;
return [p.nombre, p.cliente, p.marca, p.bu, p.solicitante]
.join(" ")
.toLowerCase()
.includes(q);
});
}, [projects, query, filter]);
const openNew = () => {
setEditing(null);
setOpen(true);
};
const openEdit = (p: Project) => {
setEditing(p);
setOpen(true);
};
const displayName = user?.displayName ?? user?.email ?? "Usuario";
const userRole = isGerardo ? "Jefe CDC" : "Usuario CDC";
const userInitials = initials(user?.displayName, user?.email);
return (
<div className="min-h-screen bg-background">
{/* Top bar */}
<header className="sticky top-0 z-30 bg-background/85 backdrop-blur-md border-b border-border">
<div className="max-w-[1400px] mx-auto px-6 py-3.5 flex items-center gap-4">
{/* Brand */}
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center">
<LayoutGrid className="w-4 h-4 text-primary-foreground" />
</div>
<div className="leading-tight">
<h1 className="text-[15px] font-semibold tracking-tight">Tablero CDC</h1>
<p className="text-[11px] text-muted-foreground">Aprobaciones de Proyectos</p>
</div>
</div>
{/* Search */}
<div className="flex-1 max-w-md ml-4 relative hidden md:block">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar proyecto, cliente, marca…"
className="pl-9 h-9 bg-muted/40 border-transparent focus-visible:bg-card focus-visible:border-border"
/>
</div>
<div className="flex-1 md:flex-none" />
{/* User menu — real authenticated user */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2 h-9 px-2.5">
<Avatar className="w-7 h-7">
{user?.photoURL && (
<AvatarImage
src={user.photoURL}
alt={displayName}
referrerPolicy="no-referrer"
/>
)}
<AvatarFallback
className={
isGerardo
? "bg-primary text-primary-foreground text-xs"
: "bg-accent text-accent-foreground text-xs"
}
>
{userInitials}
</AvatarFallback>
</Avatar>
<div className="text-left leading-tight hidden sm:block">
<div className="text-xs font-medium">{displayName}</div>
<div className="text-[10px] text-muted-foreground">{userRole}</div>
</div>
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<div className="px-3 py-2">
<p className="text-xs font-medium truncate">{displayName}</p>
<p className="text-[10px] text-muted-foreground truncate">{user?.email}</p>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={logout}
className="gap-2 text-destructive focus:text-destructive focus:bg-destructive/10 cursor-pointer"
>
<LogOut className="w-3.5 h-3.5" />
Cerrar sesión
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* New project button */}
<Button onClick={openNew} className="gap-1.5 h-9 rounded-full px-4 shadow-sm">
<Plus className="w-4 h-4" /> Nuevo
</Button>
</div>
</header>
{/* Filters */}
<div className="max-w-[1400px] mx-auto px-6 pt-6 pb-3 flex items-center justify-between gap-4 flex-wrap">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Proyectos</h2>
<p className="text-sm text-muted-foreground mt-0.5">
{filtered.length} de {projects.length}{" "}
{projects.length === 1 ? "proyecto" : "proyectos"}
</p>
</div>
<div className="inline-flex bg-muted/60 rounded-full p-1 text-xs">
{(
[
["todos", "Todos"],
["abiertos", "Activos"],
["cerrados", "Cerrados"],
] as const
).map(([k, l]) => (
<button
key={k}
onClick={() => setFilter(k)}
className={`px-3.5 py-1.5 rounded-full transition-colors font-medium ${
filter === k
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{l}
</button>
))}
</div>
</div>
{/* Grid */}
<main className="max-w-[1400px] mx-auto px-6 pb-16">
{filtered.length === 0 ? (
<EmptyState onNew={openNew} hasProjects={projects.length > 0} />
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filtered.map((p) => (
<ProjectCard key={p.id} project={p} onClick={() => openEdit(p)} />
))}
{/* Add tile */}
<button
onClick={openNew}
className="min-h-[230px] rounded-2xl border-2 border-dashed border-border hover:border-primary/50 hover:bg-primary/5 transition-colors flex flex-col items-center justify-center gap-2 text-muted-foreground hover:text-primary"
>
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
<Plus className="w-5 h-5" />
</div>
<span className="text-sm font-medium">Nuevo proyecto</span>
</button>
</div>
)}
</main>
<ProjectDialog open={open} onOpenChange={setOpen} project={editing} />
</div>
);
}
function EmptyState({ onNew, hasProjects }: { onNew: () => void; hasProjects: boolean }) {
return (
<div className="text-center py-24 rounded-2xl border border-dashed border-border bg-card/40">
<div className="w-14 h-14 rounded-2xl bg-primary/10 mx-auto flex items-center justify-center mb-4">
<LayoutGrid className="w-6 h-6 text-primary" />
</div>
<h3 className="text-lg font-semibold">
{hasProjects ? "Nada coincide con el filtro" : "Tu tablero está vacío"}
</h3>
<p className="text-sm text-muted-foreground mt-1 max-w-sm mx-auto">
{hasProjects
? "Probá quitar el filtro o cambiar la búsqueda."
: "Creá tu primer proyecto. Cuando entre un brief nuevo, abrís una caja con su nombre y ya todo el equipo lo ve."}
</p>
{!hasProjects && (
<Button onClick={onNew} className="mt-5 gap-1.5 rounded-full">
<Plus className="w-4 h-4" /> Crear proyecto
</Button>
)}
</div>
);
}