677 lines
24 KiB
TypeScript
677 lines
24 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { AlertCircle, Loader2, Lock, DollarSign, Calendar, Trash2 } from "lucide-react";
|
|
import { ColorPicker } from "./ColorPicker";
|
|
import { LinkList } from "./LinkList";
|
|
import { MESES, type Status } from "@/data/lists";
|
|
import { useAppLists } from "@/lib/appLists";
|
|
import { useProjects, type Project } from "@/lib/store";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import { colorHex } from "@/lib/colors";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
canonicalOptionLabel,
|
|
dedupeOptions,
|
|
ensureOption,
|
|
normalizeOptionKey,
|
|
} from "@/lib/optionUtils";
|
|
import { toast } from "sonner";
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (v: boolean) => void;
|
|
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 now = new Date();
|
|
return {
|
|
nombre: "",
|
|
color: "blue",
|
|
cliente: "",
|
|
bu: "",
|
|
cm: "",
|
|
marca: "",
|
|
solicitante: "",
|
|
comentarios: "",
|
|
briefLink: "",
|
|
propuestaLinks: [],
|
|
afLinks: [],
|
|
status: "",
|
|
monto: null,
|
|
mes: MESES[now.getMonth()],
|
|
anio: now.getFullYear(),
|
|
};
|
|
};
|
|
|
|
export function ProjectDialog({ open, onOpenChange, project, onDelete, deleting = false }: Props) {
|
|
const { add, update } = useProjects();
|
|
const { lists, loading: listsLoading, error: listsError, refresh: refreshLists } = useAppLists();
|
|
const { isGerardo, canDeleteProjects } = useAuth();
|
|
const isEdit = !!project;
|
|
|
|
const [form, setForm] = useState<Omit<Project, "id" | "createdAt">>(empty());
|
|
const [selectedWmcCountries, setSelectedWmcCountries] = useState<string[]>([]);
|
|
const [saving, setSaving] = useState(false);
|
|
const [savingStep, setSavingStep] = useState<string | null>(null);
|
|
const [formError, setFormError] = useState<string | null>(null);
|
|
|
|
const clienteOptions = dedupeOptions(ensureOption(lists.clientes, form.cliente));
|
|
const marcaOptions = dedupeOptions(ensureOption(lists.marcas, form.marca));
|
|
const buOptions = dedupeOptions(ensureOption(lists.bus, form.bu));
|
|
const statusOptions = dedupeOptions(ensureOption(lists.status, form.status)) as Status[];
|
|
|
|
useEffect(() => {
|
|
setFormError(null);
|
|
setSavingStep(null);
|
|
|
|
if (project) {
|
|
const { id: _i, createdAt: _c, ...rest } = project;
|
|
const cleanBu = canonicalOptionLabel(rest.bu);
|
|
setForm({
|
|
...rest,
|
|
bu: cleanBu,
|
|
});
|
|
setSelectedWmcCountries(cleanBu ? [cleanBu] : []);
|
|
} else if (open) {
|
|
setForm(empty());
|
|
setSelectedWmcCountries([]);
|
|
}
|
|
}, [project, open]);
|
|
|
|
const set = <K extends keyof typeof form>(k: K, v: (typeof form)[K]) =>
|
|
setForm((f) => ({ ...f, [k]: v }));
|
|
|
|
const getCountryManager = (bu: string) => {
|
|
const direct = lists.buCm[bu];
|
|
if (direct) return direct;
|
|
|
|
const buKey = normalizeOptionKey(bu);
|
|
const match = Object.entries(lists.buCm).find(([key]) => normalizeOptionKey(key) === buKey);
|
|
|
|
return match?.[1] ?? "";
|
|
};
|
|
|
|
// BU → Country Manager automático desde Supabase tablero_cdc_app_lists, con fallback local.
|
|
const onBU = (bu: string) => {
|
|
const cleanBu = canonicalOptionLabel(bu);
|
|
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 =
|
|
form.nombre.trim() &&
|
|
form.cliente &&
|
|
form.marca &&
|
|
form.solicitante.trim() &&
|
|
(isWalmartConnectCreate ? selectedWmcCountries.length > 0 : form.bu);
|
|
|
|
const showSaveToast = (result: Awaited<ReturnType<typeof add>>) => {
|
|
if (result.sheetSyncStatus === "synced") {
|
|
toast.success(isEdit ? "Proyecto actualizado" : "Proyecto creado", {
|
|
description: "Se guardó y se sincronizó con el Google Sheet.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (result.sheetSyncStatus === "skipped") {
|
|
toast.success(isEdit ? "Proyecto actualizado" : "Proyecto creado", {
|
|
description: "Se guardó. La sincronización con Google Sheet no está configurada.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
toast.warning("Proyecto guardado con aviso", {
|
|
description: result.sheetSyncMessage,
|
|
});
|
|
};
|
|
|
|
const submit = async () => {
|
|
if (!requiredOk || saving) return;
|
|
|
|
const safeForm = isGerardo
|
|
? form
|
|
: {
|
|
...form,
|
|
status: project?.status ?? "",
|
|
monto: project?.monto ?? null,
|
|
};
|
|
|
|
try {
|
|
setSaving(true);
|
|
setFormError(null);
|
|
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 =
|
|
isEdit && project
|
|
? await update(project.id, safeForm)
|
|
: await add({ ...safeForm, id: crypto.randomUUID(), createdAt: Date.now() });
|
|
|
|
showSaveToast(result);
|
|
}
|
|
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
console.error("Error guardando proyecto:", error);
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "No se pudo guardar el proyecto. Revisa los permisos o intenta de nuevo.";
|
|
|
|
setFormError(message);
|
|
toast.error("No se pudo guardar el proyecto", {
|
|
description: message,
|
|
});
|
|
} finally {
|
|
setSaving(false);
|
|
setSavingStep(null);
|
|
}
|
|
};
|
|
|
|
const closed = !!form.status;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-3xl max-h-[92vh] overflow-y-auto p-0 gap-0">
|
|
{/* Pestaña de color superior */}
|
|
<div
|
|
className="h-2.5 w-full rounded-t-lg"
|
|
style={{ backgroundColor: colorHex(form.color) }}
|
|
/>
|
|
|
|
<div className="px-7 pt-6 pb-7">
|
|
<DialogHeader className="space-y-3">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1">
|
|
<DialogTitle className="text-2xl tracking-tight">
|
|
{isEdit ? form.nombre || "Sin título" : "Nuevo proyecto"}
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-1 flex items-center gap-2 text-xs">
|
|
<Calendar className="w-3.5 h-3.5" />
|
|
{form.mes} · {form.anio}
|
|
{closed && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
Cerrado
|
|
</Badge>
|
|
)}
|
|
</DialogDescription>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 mt-6">
|
|
{/* Nombre */}
|
|
<div className="md:col-span-2 space-y-1.5">
|
|
<Label>
|
|
Nombre del proyecto <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={form.nombre}
|
|
onChange={(e) => set("nombre", e.target.value)}
|
|
placeholder="Ej: Promoción Día de las Madres"
|
|
className="h-10"
|
|
/>
|
|
</div>
|
|
|
|
{/* Color */}
|
|
<div className="md:col-span-2 space-y-2">
|
|
<Label>Etiqueta de color</Label>
|
|
<ColorPicker value={form.color} onChange={(c) => set("color", c)} />
|
|
</div>
|
|
|
|
<Separator className="md:col-span-2 my-1" />
|
|
|
|
{(listsLoading || listsError) && (
|
|
<div
|
|
className={cn(
|
|
"md:col-span-2 flex items-center justify-between gap-3 rounded-xl border px-3 py-2 text-xs",
|
|
listsError
|
|
? "border-amber-300/50 bg-amber-50 text-amber-900"
|
|
: "border-border bg-muted/30 text-muted-foreground",
|
|
)}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
{listsLoading ? (
|
|
<>
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
Cargando listas…
|
|
</>
|
|
) : (
|
|
<>
|
|
<AlertCircle className="h-3.5 w-3.5" />
|
|
No se pudieron cargar las listas. Se usaron opciones locales.
|
|
</>
|
|
)}
|
|
</span>
|
|
|
|
{listsError && (
|
|
<button
|
|
type="button"
|
|
onClick={() => void refreshLists()}
|
|
className="font-medium text-amber-950 underline-offset-4 hover:underline"
|
|
>
|
|
Reintentar
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Cliente */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Cliente <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select value={form.cliente} onValueChange={onCliente}>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="Seleccionar…" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{clienteOptions.map((c) => (
|
|
<SelectItem key={c} value={c}>
|
|
{c}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Marca */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Marca <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select value={form.marca} onValueChange={(v) => set("marca", v)}>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="Seleccionar…" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{marcaOptions.map((m) => (
|
|
<SelectItem key={m} value={m}>
|
|
{m}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 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">
|
|
<Label>
|
|
BU Solicita (País) <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select value={form.bu} onValueChange={onBU}>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="Seleccionar…" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{buOptions.map((b) => (
|
|
<SelectItem key={b} value={b}>
|
|
{b}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
{/* CM autocompletado */}
|
|
<div className={cn("space-y-1.5", isWalmartConnectCreate && "md:col-span-2")}>
|
|
<Label className="flex items-center gap-1.5">
|
|
Country Manager <span className="text-xs text-muted-foreground">(automático)</span>
|
|
</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
|
|
value={form.cm}
|
|
readOnly
|
|
disabled
|
|
className="h-10 bg-muted/50"
|
|
placeholder="—"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Solicitante */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Solicitado por <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
value={form.solicitante}
|
|
onChange={(e) => set("solicitante", e.target.value)}
|
|
placeholder="Nombre de quien solicita"
|
|
className="h-10"
|
|
/>
|
|
</div>
|
|
|
|
{/* Comentarios */}
|
|
<div className="md:col-span-2 space-y-1.5">
|
|
<Label>Comentarios</Label>
|
|
<Textarea
|
|
value={form.comentarios}
|
|
onChange={(e) => set("comentarios", e.target.value)}
|
|
placeholder="Notas adicionales del proyecto…"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
|
|
<Separator className="md:col-span-2 my-1" />
|
|
|
|
{/* Brief link único */}
|
|
<div className="md:col-span-2 space-y-1.5">
|
|
<Label>
|
|
Link del Brief{" "}
|
|
<span className="text-xs text-muted-foreground font-normal">
|
|
(carpeta de Fulgencio)
|
|
</span>
|
|
</Label>
|
|
<Input
|
|
value={form.briefLink}
|
|
onChange={(e) => set("briefLink", e.target.value)}
|
|
placeholder="https://drive.google.com/…"
|
|
className="h-10"
|
|
/>
|
|
</div>
|
|
|
|
{/* Propuestas */}
|
|
<div className="md:col-span-2">
|
|
<LinkList
|
|
label="Links de Propuestas"
|
|
links={form.propuestaLinks}
|
|
onChange={(v) => set("propuestaLinks", v)}
|
|
emptyText="Aún no hay propuestas. Pega tantos links como necesites."
|
|
/>
|
|
</div>
|
|
|
|
{/* Artes Finales */}
|
|
<div className="md:col-span-2">
|
|
<LinkList
|
|
label="Links de Artes Finales"
|
|
prefix="AF"
|
|
links={form.afLinks}
|
|
onChange={(v) => set("afLinks", v)}
|
|
emptyText="Aún no hay artes finales."
|
|
/>
|
|
</div>
|
|
|
|
<Separator className="md:col-span-2 my-1" />
|
|
|
|
{/* Status: Gerardo edita; el equipo solo visualiza */}
|
|
<div className="space-y-1.5">
|
|
<Label className="flex items-center gap-1.5">
|
|
Estatus
|
|
{!isGerardo && <Lock className="w-3 h-3 text-muted-foreground" />}
|
|
</Label>
|
|
|
|
{isGerardo ? (
|
|
<Select
|
|
value={form.status || "_none"}
|
|
onValueChange={(v) => set("status", (v === "_none" ? "" : v) as Status | "")}
|
|
>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="Sin estatus" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="_none">Sin estatus</SelectItem>
|
|
{statusOptions.map((s) => (
|
|
<SelectItem key={s} value={s}>
|
|
{s}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<div className="h-10 px-3 flex items-center rounded-md border border-border bg-muted/30 text-sm text-foreground">
|
|
{form.status || "Sin estatus"}
|
|
</div>
|
|
)}
|
|
|
|
{!isGerardo && (
|
|
<p className="text-[11px] text-muted-foreground">
|
|
Solo Gerardo Marrero puede cambiar el estatus.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Monto: SOLO visible para Gerardo Marrero */}
|
|
{isGerardo && (
|
|
<div className="space-y-1.5">
|
|
<Label className="flex items-center gap-1.5">
|
|
<DollarSign className="w-3.5 h-3.5" /> Interno Cargado
|
|
</Label>
|
|
<Input
|
|
type="number"
|
|
value={form.monto ?? ""}
|
|
onChange={(e) =>
|
|
set("monto", e.target.value === "" ? null : Number(e.target.value))
|
|
}
|
|
placeholder="0.00"
|
|
className="h-10"
|
|
/>
|
|
<p className="text-[11px] text-muted-foreground">Privado · solo Gerardo Marrero.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{(formError || savingStep) && (
|
|
<div
|
|
className={cn(
|
|
"mt-5 flex items-start gap-2 rounded-xl border px-3 py-2 text-sm",
|
|
formError
|
|
? "border-destructive/30 bg-destructive/5 text-destructive"
|
|
: "border-primary/20 bg-primary/5 text-primary",
|
|
)}
|
|
>
|
|
{formError ? (
|
|
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
|
) : (
|
|
<Loader2 className="mt-0.5 h-4 w-4 flex-shrink-0 animate-spin" />
|
|
)}
|
|
<span>{formError || savingStep}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter className="px-7 py-4 border-t bg-muted/30 rounded-b-lg sm:justify-between">
|
|
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<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
|
|
</Button>
|
|
<Button onClick={submit} disabled={!requiredOk || saving || deleting}>
|
|
{saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
|
{saving
|
|
? "Guardando…"
|
|
: isEdit
|
|
? "Guardar cambios"
|
|
: isWalmartConnectCreate
|
|
? "Crear proyectos"
|
|
: "Crear proyecto"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|