feat: agregar eliminacion y Walmart Connect multipais
This commit is contained in:
@@ -5,15 +5,30 @@ import { colorHex } from "@/lib/colors";
|
||||
import type { Project } from "@/lib/store";
|
||||
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 { isGerardo } = useAuth();
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
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",
|
||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
closed && "opacity-65 hover:opacity-90",
|
||||
@@ -86,7 +101,7 @@ export function ProjectCard({ project, onClick }: { project: Project; onClick: (
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ 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,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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 { LinkList } from "./LinkList";
|
||||
import { MESES, type Status } from "@/data/lists";
|
||||
@@ -41,6 +42,14 @@ 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"> => {
|
||||
@@ -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 { lists, loading: listsLoading, error: listsError, refresh: refreshLists } = useAppLists();
|
||||
const { isGerardo } = useAuth();
|
||||
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);
|
||||
@@ -86,12 +96,15 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
|
||||
|
||||
if (project) {
|
||||
const { id: _i, createdAt: _c, ...rest } = project;
|
||||
const cleanBu = canonicalOptionLabel(rest.bu);
|
||||
setForm({
|
||||
...rest,
|
||||
bu: canonicalOptionLabel(rest.bu),
|
||||
bu: cleanBu,
|
||||
});
|
||||
setSelectedWmcCountries(cleanBu ? [cleanBu] : []);
|
||||
} else if (open) {
|
||||
setForm(empty());
|
||||
setSelectedWmcCountries([]);
|
||||
}
|
||||
}, [project, open]);
|
||||
|
||||
@@ -114,8 +127,51 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
|
||||
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.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>>) => {
|
||||
if (result.sheetSyncStatus === "synced") {
|
||||
@@ -153,12 +209,46 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
|
||||
setFormError(null);
|
||||
setSavingStep("Guardando y sincronizando con Google Sheet…");
|
||||
|
||||
const result =
|
||||
isEdit && project
|
||||
? await update(project.id, safeForm)
|
||||
: await add({ ...safeForm, id: crypto.randomUUID(), createdAt: Date.now() });
|
||||
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);
|
||||
}
|
||||
|
||||
showSaveToast(result);
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("Error guardando proyecto:", error);
|
||||
@@ -270,7 +360,7 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
|
||||
<Label>
|
||||
Cliente <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select value={form.cliente} onValueChange={(v) => set("cliente", v)}>
|
||||
<Select value={form.cliente} onValueChange={onCliente}>
|
||||
<SelectTrigger className="h-10">
|
||||
<SelectValue placeholder="Seleccionar…" />
|
||||
</SelectTrigger>
|
||||
@@ -304,36 +394,101 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
|
||||
</div>
|
||||
|
||||
{/* BU */}
|
||||
<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>
|
||||
{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="space-y-1.5">
|
||||
<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>
|
||||
<Input
|
||||
value={form.cm}
|
||||
readOnly
|
||||
disabled
|
||||
className="h-10 bg-muted/50"
|
||||
placeholder="—"
|
||||
/>
|
||||
{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 */}
|
||||
@@ -477,15 +632,42 @@ export function ProjectDialog({ open, onOpenChange, project }: Props) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-7 py-4 border-t bg-muted/30 rounded-b-lg flex-row justify-end sm:justify-end">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={saving}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={submit} disabled={!requiredOk || saving}>
|
||||
{saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
||||
{saving ? "Guardando…" : isEdit ? "Guardar cambios" : "Crear proyecto"}
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user