chore: subir version inicial del Tablero CDC
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user