feat: agregar eliminacion y Walmart Connect multipais

This commit is contained in:
2026-06-05 15:59:54 -04:00
parent c546f59009
commit 8c3d18814d
10 changed files with 720 additions and 404 deletions
File diff suppressed because one or more lines are too long
-341
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+348
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<meta name="description" content="Herramienta interna del Departamento de Diseño Creativo." /> <meta name="description" content="Herramienta interna del Departamento de Diseño Creativo." />
<meta name="author" content="CDC" /> <meta name="author" content="CDC" />
<link rel="icon" type="image/svg+xml" href="/tablero-cdc/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/tablero-cdc/favicon.svg" />
<script type="module" crossorigin src="/tablero-cdc/assets/index-BEinrcZb.js"></script> <script type="module" crossorigin src="/tablero-cdc/assets/index-ZKG15NE5.js"></script>
<link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-DjXLQf1G.css"> <link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-7ZtFhhZY.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+19 -4
View File
@@ -5,15 +5,30 @@ import { colorHex } from "@/lib/colors";
import type { Project } from "@/lib/store"; import type { Project } from "@/lib/store";
import { useAuth } from "@/context/AuthContext"; import { useAuth } from "@/context/AuthContext";
export function ProjectCard({ project, onClick }: { project: Project; onClick: () => void }) { interface ProjectCardProps {
project: Project;
onClick: () => void;
}
export function ProjectCard({ project, onClick }: ProjectCardProps) {
const closed = !!project.status; const closed = !!project.status;
const { isGerardo } = useAuth(); const { isGerardo } = useAuth();
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onClick();
}
};
return ( return (
<button <div
role="button"
tabIndex={0}
onClick={onClick} onClick={onClick}
onKeyDown={handleKeyDown}
className={cn( className={cn(
"group block text-left w-full rounded-2xl bg-card overflow-hidden p-0 ring-1 ring-border", "group block text-left w-full rounded-2xl bg-card overflow-hidden p-0 ring-1 ring-border cursor-pointer",
"transition-all duration-200 hover:-translate-y-0.5", "transition-all duration-200 hover:-translate-y-0.5",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
closed && "opacity-65 hover:opacity-90", closed && "opacity-65 hover:opacity-90",
@@ -86,7 +101,7 @@ export function ProjectCard({ project, onClick }: { project: Project; onClick: (
</div> </div>
</div> </div>
</div> </div>
</button> </div>
); );
} }
+194 -12
View File
@@ -11,6 +11,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -20,7 +21,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { AlertCircle, Loader2, Lock, DollarSign, Calendar } from "lucide-react"; import { AlertCircle, Loader2, Lock, DollarSign, Calendar, Trash2 } from "lucide-react";
import { ColorPicker } from "./ColorPicker"; import { ColorPicker } from "./ColorPicker";
import { LinkList } from "./LinkList"; import { LinkList } from "./LinkList";
import { MESES, type Status } from "@/data/lists"; import { MESES, type Status } from "@/data/lists";
@@ -41,6 +42,14 @@ interface Props {
open: boolean; open: boolean;
onOpenChange: (v: boolean) => void; onOpenChange: (v: boolean) => void;
project?: Project | null; // editar; vacío = crear project?: Project | null; // editar; vacío = crear
onDelete?: (project: Project) => void;
deleting?: boolean;
}
const WALMART_CONNECT_CLIENT = "Walmart Connect WMC";
function isWalmartConnectClient(client: string | null | undefined) {
return normalizeOptionKey(client) === normalizeOptionKey(WALMART_CONNECT_CLIENT);
} }
const empty = (): Omit<Project, "id" | "createdAt"> => { const empty = (): Omit<Project, "id" | "createdAt"> => {
@@ -64,13 +73,14 @@ const empty = (): Omit<Project, "id" | "createdAt"> => {
}; };
}; };
export function ProjectDialog({ open, onOpenChange, project }: Props) { export function ProjectDialog({ open, onOpenChange, project, onDelete, deleting = false }: Props) {
const { add, update } = useProjects(); const { add, update } = useProjects();
const { lists, loading: listsLoading, error: listsError, refresh: refreshLists } = useAppLists(); const { lists, loading: listsLoading, error: listsError, refresh: refreshLists } = useAppLists();
const { isGerardo } = useAuth(); const { isGerardo, canDeleteProjects } = useAuth();
const isEdit = !!project; const isEdit = !!project;
const [form, setForm] = useState<Omit<Project, "id" | "createdAt">>(empty()); const [form, setForm] = useState<Omit<Project, "id" | "createdAt">>(empty());
const [selectedWmcCountries, setSelectedWmcCountries] = useState<string[]>([]);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [savingStep, setSavingStep] = useState<string | null>(null); const [savingStep, setSavingStep] = useState<string | null>(null);
const [formError, setFormError] = useState<string | null>(null); const [formError, setFormError] = useState<string | null>(null);
@@ -86,12 +96,15 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
if (project) { if (project) {
const { id: _i, createdAt: _c, ...rest } = project; const { id: _i, createdAt: _c, ...rest } = project;
const cleanBu = canonicalOptionLabel(rest.bu);
setForm({ setForm({
...rest, ...rest,
bu: canonicalOptionLabel(rest.bu), bu: cleanBu,
}); });
setSelectedWmcCountries(cleanBu ? [cleanBu] : []);
} else if (open) { } else if (open) {
setForm(empty()); setForm(empty());
setSelectedWmcCountries([]);
} }
}, [project, open]); }, [project, open]);
@@ -114,8 +127,51 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
setForm((f) => ({ ...f, bu: cleanBu, cm: getCountryManager(cleanBu) })); setForm((f) => ({ ...f, bu: cleanBu, cm: getCountryManager(cleanBu) }));
}; };
const onCliente = (cliente: string) => {
const cleanCliente = canonicalOptionLabel(cliente);
setForm((f) => ({
...f,
cliente: cleanCliente,
...(isWalmartConnectClient(cleanCliente) && !isEdit
? { bu: "", cm: "" }
: f.cliente && isWalmartConnectClient(f.cliente)
? { bu: "", cm: "" }
: {}),
}));
if (!isWalmartConnectClient(cleanCliente)) {
setSelectedWmcCountries([]);
}
};
const isWalmartConnectCreate = !isEdit && isWalmartConnectClient(form.cliente);
const toggleWmcCountry = (country: string, checked: boolean) => {
const cleanCountry = canonicalOptionLabel(country);
setSelectedWmcCountries((current) => {
if (checked) {
return dedupeOptions([...current, cleanCountry]);
}
return current.filter(
(item) => normalizeOptionKey(item) !== normalizeOptionKey(cleanCountry),
);
});
};
const selectedWmcCountryManagers = selectedWmcCountries.map((country) => ({
country,
manager: getCountryManager(country),
}));
const requiredOk = const requiredOk =
form.nombre.trim() && form.cliente && form.bu && form.marca && form.solicitante.trim(); form.nombre.trim() &&
form.cliente &&
form.marca &&
form.solicitante.trim() &&
(isWalmartConnectCreate ? selectedWmcCountries.length > 0 : form.bu);
const showSaveToast = (result: Awaited<ReturnType<typeof add>>) => { const showSaveToast = (result: Awaited<ReturnType<typeof add>>) => {
if (result.sheetSyncStatus === "synced") { if (result.sheetSyncStatus === "synced") {
@@ -153,12 +209,46 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
setFormError(null); setFormError(null);
setSavingStep("Guardando y sincronizando con Google Sheet…"); setSavingStep("Guardando y sincronizando con Google Sheet…");
if (isWalmartConnectCreate) {
const countries = selectedWmcCountries;
const results = [];
for (const [index, country] of countries.entries()) {
setSavingStep(
`Guardando Walmart Connect WMC ${index + 1} de ${countries.length}: ${country}`,
);
const countryProject = {
...safeForm,
bu: country,
cm: getCountryManager(country),
id: crypto.randomUUID(),
createdAt: Date.now() + index,
};
results.push(await add(countryProject));
}
const failedSyncs = results.filter((result) => result.sheetSyncStatus !== "synced");
if (failedSyncs.length === 0) {
toast.success("Proyectos Walmart Connect creados", {
description: `Se crearon ${countries.length} proyectos y ${countries.length} filas en Google Sheet.`,
});
} else {
toast.warning("Proyectos Walmart Connect creados con aviso", {
description: `${failedSyncs.length} de ${countries.length} sincronizaciones con Google Sheet requieren revisión.`,
});
}
} else {
const result = const result =
isEdit && project isEdit && project
? await update(project.id, safeForm) ? await update(project.id, safeForm)
: await add({ ...safeForm, id: crypto.randomUUID(), createdAt: Date.now() }); : await add({ ...safeForm, id: crypto.randomUUID(), createdAt: Date.now() });
showSaveToast(result); showSaveToast(result);
}
onOpenChange(false); onOpenChange(false);
} catch (error) { } catch (error) {
console.error("Error guardando proyecto:", error); console.error("Error guardando proyecto:", error);
@@ -270,7 +360,7 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
<Label> <Label>
Cliente <span className="text-destructive">*</span> Cliente <span className="text-destructive">*</span>
</Label> </Label>
<Select value={form.cliente} onValueChange={(v) => set("cliente", v)}> <Select value={form.cliente} onValueChange={onCliente}>
<SelectTrigger className="h-10"> <SelectTrigger className="h-10">
<SelectValue placeholder="Seleccionar…" /> <SelectValue placeholder="Seleccionar…" />
</SelectTrigger> </SelectTrigger>
@@ -304,6 +394,54 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
</div> </div>
{/* BU */} {/* BU */}
{isWalmartConnectCreate ? (
<div className="md:col-span-2 space-y-2 rounded-xl border border-primary/20 bg-primary/5 p-3">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<Label>
BU Solicita (Países) <span className="text-destructive">*</span>
</Label>
<span className="text-xs font-medium text-primary">
{selectedWmcCountries.length} seleccionado(s)
</span>
</div>
<p className="text-xs text-muted-foreground">
Para Walmart Connect WMC puedes seleccionar varios países. La app creará un
proyecto y una fila del Sheet por cada país.
</p>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{buOptions.map((b) => {
const checked = selectedWmcCountries.some(
(country) => normalizeOptionKey(country) === normalizeOptionKey(b),
);
const manager = getCountryManager(b);
return (
<label
key={b}
className={cn(
"flex cursor-pointer items-start gap-2 rounded-lg border bg-card px-3 py-2 text-sm transition-colors",
checked
? "border-primary/50 bg-primary/10"
: "border-border hover:bg-muted/40",
)}
>
<Checkbox
checked={checked}
onCheckedChange={(value) => toggleWmcCountry(b, value === true)}
className="mt-0.5"
/>
<span className="leading-tight">
<span className="block font-medium">{b}</span>
<span className="text-xs text-muted-foreground">
CM: {manager || "—"}
</span>
</span>
</label>
);
})}
</div>
</div>
) : (
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label> <Label>
BU Solicita (País) <span className="text-destructive">*</span> BU Solicita (País) <span className="text-destructive">*</span>
@@ -321,12 +459,28 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
{/* CM autocompletado */} {/* CM autocompletado */}
<div className="space-y-1.5"> <div className={cn("space-y-1.5", isWalmartConnectCreate && "md:col-span-2")}>
<Label className="flex items-center gap-1.5"> <Label className="flex items-center gap-1.5">
Country Manager <span className="text-xs text-muted-foreground">(automático)</span> Country Manager <span className="text-xs text-muted-foreground">(automático)</span>
</Label> </Label>
{isWalmartConnectCreate ? (
<div className="min-h-10 rounded-md border border-border bg-muted/30 px-3 py-2 text-sm">
{selectedWmcCountryManagers.length === 0 ? (
<span className="text-muted-foreground">Selecciona uno o más países.</span>
) : (
<div className="flex flex-wrap gap-1.5">
{selectedWmcCountryManagers.map(({ country, manager }) => (
<Badge key={country} variant="secondary" className="font-normal">
{country}: {manager || "—"}
</Badge>
))}
</div>
)}
</div>
) : (
<Input <Input
value={form.cm} value={form.cm}
readOnly readOnly
@@ -334,6 +488,7 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
className="h-10 bg-muted/50" className="h-10 bg-muted/50"
placeholder="—" placeholder="—"
/> />
)}
</div> </div>
{/* Solicitante */} {/* Solicitante */}
@@ -477,16 +632,43 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
)} )}
</div> </div>
<DialogFooter className="px-7 py-4 border-t bg-muted/30 rounded-b-lg flex-row justify-end sm:justify-end"> <DialogFooter className="px-7 py-4 border-t bg-muted/30 rounded-b-lg sm:justify-between">
<div className="flex gap-2"> <div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={saving}> <div>
{isEdit && project && canDeleteProjects && onDelete && (
<Button
type="button"
variant="destructive"
onClick={() => onDelete(project)}
disabled={saving || deleting}
className="gap-1.5 rounded-full"
>
{deleting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
{deleting ? "Eliminando…" : "Eliminar proyecto"}
</Button>
)}
</div>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={saving || deleting}>
Cancelar Cancelar
</Button> </Button>
<Button onClick={submit} disabled={!requiredOk || saving}> <Button onClick={submit} disabled={!requiredOk || saving || deleting}>
{saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />} {saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
{saving ? "Guardando…" : isEdit ? "Guardar cambios" : "Crear proyecto"} {saving
? "Guardando…"
: isEdit
? "Guardar cambios"
: isWalmartConnectCreate
? "Crear proyectos"
: "Crear proyecto"}
</Button> </Button>
</div> </div>
</div>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+11 -2
View File
@@ -12,6 +12,7 @@ import { supabase } from "@/lib/supabase";
const ALLOWED_DOMAIN = "gomezleemarketing.com"; const ALLOWED_DOMAIN = "gomezleemarketing.com";
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com"; const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
const DELETE_ALLOWED_EMAILS = [GERARDO_EMAIL, "areyes@gomezleemarketing.com"];
function isAllowedEmail(email: string | null | undefined): boolean { function isAllowedEmail(email: string | null | undefined): boolean {
if (!email) return false; if (!email) return false;
@@ -59,6 +60,7 @@ interface AuthContextValue {
loading: boolean; loading: boolean;
error: string | null; error: string | null;
isGerardo: boolean; isGerardo: boolean;
canDeleteProjects: boolean;
loginWithGoogle: () => Promise<void>; loginWithGoogle: () => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
@@ -158,10 +160,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setError(null); setError(null);
}, []); }, []);
const isGerardo = useMemo(() => !!user && user.email?.toLowerCase() === GERARDO_EMAIL, [user]); const normalizedEmail = user?.email?.toLowerCase() || "";
const isGerardo = useMemo(() => normalizedEmail === GERARDO_EMAIL, [normalizedEmail]);
const canDeleteProjects = useMemo(
() => DELETE_ALLOWED_EMAILS.includes(normalizedEmail),
[normalizedEmail],
);
return ( return (
<AuthContext.Provider value={{ user, loading, error, isGerardo, loginWithGoogle, logout }}> <AuthContext.Provider
value={{ user, loading, error, isGerardo, canDeleteProjects, loginWithGoogle, logout }}
>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
+69 -5
View File
@@ -57,6 +57,7 @@ type DbProject = {
const DEFAULT_COLOR: ColorId = "blue"; const DEFAULT_COLOR: ColorId = "blue";
const OPEN_STATUS = "Activo"; const OPEN_STATUS = "Activo";
const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com"; const GERARDO_EMAIL = "gmarrero@gomezleemarketing.com";
const DELETE_ALLOWED_EMAILS = [GERARDO_EMAIL, "areyes@gomezleemarketing.com"];
let listeners: Array<() => void> = []; let listeners: Array<() => void> = [];
let cache: Project[] = []; let cache: Project[] = [];
@@ -146,6 +147,11 @@ async function currentUserIsGerardo(): Promise<boolean> {
return user.email?.toLowerCase() === GERARDO_EMAIL; return user.email?.toLowerCase() === GERARDO_EMAIL;
} }
async function currentUserCanDeleteProjects(): Promise<boolean> {
const user = await getCurrentUser();
return DELETE_ALLOWED_EMAILS.includes(user.email?.toLowerCase() || "");
}
function toProjectRow(project: Omit<Project, "id" | "createdAt"> | Partial<Project>) { function toProjectRow(project: Omit<Project, "id" | "createdAt"> | Partial<Project>) {
return { return {
title: project.nombre, title: project.nombre,
@@ -207,7 +213,7 @@ function getSyncWebhookUrl() {
async function syncProjectToSheet( async function syncProjectToSheet(
projectId: string, projectId: string,
action: "create" | "update", action: "create" | "update" | "delete",
): Promise<Omit<SaveProjectResult, "projectId">> { ): Promise<Omit<SaveProjectResult, "projectId">> {
const webhookUrl = getSyncWebhookUrl(); const webhookUrl = getSyncWebhookUrl();
@@ -215,7 +221,10 @@ async function syncProjectToSheet(
console.info("No se configuró VITE_WEBHOOK_URL; se omite sincronización con Google Sheet."); console.info("No se configuró VITE_WEBHOOK_URL; se omite sincronización con Google Sheet.");
return { return {
sheetSyncStatus: "skipped", sheetSyncStatus: "skipped",
sheetSyncMessage: "Proyecto guardado. No hay webhook configurado para sincronizar el Sheet.", sheetSyncMessage:
action === "delete"
? "No hay webhook configurado para eliminar el proyecto del Google Sheet."
: "Proyecto guardado. No hay webhook configurado para sincronizar el Sheet.",
}; };
} }
@@ -249,13 +258,19 @@ async function syncProjectToSheet(
return { return {
sheetSyncStatus: "failed", sheetSyncStatus: "failed",
sheetSyncMessage: `Proyecto guardado, pero el Sheet no se pudo sincronizar: ${message}`, sheetSyncMessage:
action === "delete"
? `No se pudo eliminar el proyecto del Google Sheet: ${message}`
: `Proyecto guardado, pero el Sheet no se pudo sincronizar: ${message}`,
}; };
} }
return { return {
sheetSyncStatus: "synced", sheetSyncStatus: "synced",
sheetSyncMessage: "Proyecto guardado y sincronizado con el Google Sheet.", sheetSyncMessage:
action === "delete"
? "Proyecto eliminado del Google Sheet."
: "Proyecto guardado y sincronizado con el Google Sheet.",
}; };
} catch (error) { } catch (error) {
const isTimeout = error instanceof DOMException && error.name === "AbortError"; const isTimeout = error instanceof DOMException && error.name === "AbortError";
@@ -265,7 +280,11 @@ async function syncProjectToSheet(
return { return {
sheetSyncStatus: isTimeout ? "timeout" : "failed", sheetSyncStatus: isTimeout ? "timeout" : "failed",
sheetSyncMessage: isTimeout sheetSyncMessage: isTimeout
? "Proyecto guardado, pero n8n tardó demasiado en responder. Revisa el workflow si el Sheet no se actualizó." ? action === "delete"
? "n8n tardó demasiado en responder. No se eliminó el proyecto para evitar inconsistencias."
: "Proyecto guardado, pero n8n tardó demasiado en responder. Revisa el workflow si el Sheet no se actualizó."
: action === "delete"
? "No se pudo eliminar el proyecto del Google Sheet. No se eliminó el proyecto para evitar inconsistencias."
: "Proyecto guardado, pero no se pudo sincronizar con Google Sheet.", : "Proyecto guardado, pero no se pudo sincronizar con Google Sheet.",
}; };
} finally { } finally {
@@ -465,6 +484,50 @@ async function updateProject(id: string, patch: Partial<Project>) {
} }
async function deleteProject(id: string) {
setError(null);
const canDelete = await currentUserCanDeleteProjects();
if (!canDelete) {
const message = "No tienes permiso para eliminar proyectos.";
setError(message);
throw new Error(message);
}
const sheetSync = await syncProjectToSheet(id, "delete");
if (sheetSync.sheetSyncStatus !== "synced") {
const message = sheetSync.sheetSyncMessage || "No se pudo eliminar el proyecto del Google Sheet.";
setError(message);
throw new Error(message);
}
const { error: linksError } = await supabase
.from("tablero_cdc_project_links")
.delete()
.eq("project_id", id);
if (linksError) {
setError(linksError.message);
throw linksError;
}
const { error: projectError } = await supabase
.from("tablero_cdc_projects")
.delete()
.eq("id", id);
if (projectError) {
setError(projectError.message);
throw projectError;
}
setCache(cache.filter((project) => project.id !== id));
await loadProjects();
}
export function useProjects() { export function useProjects() {
const [, force] = useState(0); const [, force] = useState(0);
@@ -496,6 +559,7 @@ export function useProjects() {
refresh: loadProjects, refresh: loadProjects,
add: addProject, add: addProject,
update: updateProject, update: updateProject,
remove: deleteProject,
}; };
} }
+42 -3
View File
@@ -39,7 +39,7 @@ function initials(name: string | null | undefined, email: string | null | undefi
} }
export default function BoardPage() { export default function BoardPage() {
const { projects, loading, error, refresh } = useProjects(); const { projects, loading, error, refresh, remove } = useProjects();
const { user, isGerardo, logout } = useAuth(); const { user, isGerardo, logout } = useAuth();
const projectsTopRef = useRef<HTMLDivElement | null>(null); const projectsTopRef = useRef<HTMLDivElement | null>(null);
@@ -53,6 +53,7 @@ export default function BoardPage() {
abiertos: 1, abiertos: 1,
cerrados: 1, cerrados: 1,
}); });
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(null);
const filtered = useMemo(() => { const filtered = useMemo(() => {
const q = query.trim().toLowerCase(); const q = query.trim().toLowerCase();
@@ -105,6 +106,34 @@ export default function BoardPage() {
setOpen(true); setOpen(true);
}; };
const deleteProject = async (project: Project) => {
const confirmed = window.confirm(
`¿Seguro que deseas eliminar el proyecto "${project.nombre || "Sin título"}"?\n\nEsta acción eliminará el proyecto del tablero y del Google Sheet.`,
);
if (!confirmed) return;
setDeletingProjectId(project.id);
try {
await remove(project.id);
if (editing?.id === project.id) {
setOpen(false);
setEditing(null);
}
} catch (err) {
console.error("Error eliminando proyecto:", err);
const message =
err instanceof Error
? err.message
: "No se pudo eliminar el proyecto. Revisa n8n, Supabase o permisos.";
window.alert(message);
} finally {
setDeletingProjectId(null);
}
};
const displayName = user?.displayName ?? user?.email ?? "Usuario"; const displayName = user?.displayName ?? user?.email ?? "Usuario";
const userRole = isGerardo ? "Jefe CDC" : "Usuario CDC"; const userRole = isGerardo ? "Jefe CDC" : "Usuario CDC";
const userInitials = initials(user?.displayName, user?.email); const userInitials = initials(user?.displayName, user?.email);
@@ -256,7 +285,11 @@ export default function BoardPage() {
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{paginatedProjects.map((p) => ( {paginatedProjects.map((p) => (
<ProjectCard key={p.id} project={p} onClick={() => openEdit(p)} /> <ProjectCard
key={p.id}
project={p}
onClick={() => openEdit(p)}
/>
))} ))}
</div> </div>
)} )}
@@ -273,7 +306,13 @@ export default function BoardPage() {
)} )}
</main> </main>
<ProjectDialog open={open} onOpenChange={setOpen} project={editing} /> <ProjectDialog
open={open}
onOpenChange={setOpen}
project={editing}
onDelete={deleteProject}
deleting={!!editing && deletingProjectId === editing.id}
/>
</div> </div>
); );
} }