376 lines
13 KiB
TypeScript
376 lines
13 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 {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Lock, Trash2, DollarSign, Calendar } from "lucide-react";
|
|
import { ColorPicker } from "./ColorPicker";
|
|
import { LinkList } from "./LinkList";
|
|
import { CLIENTES, BUS, BU_CM, MARCAS, STATUS, COLORS, MESES, type Status } from "@/data/lists";
|
|
import { useProjects, type Project } from "@/lib/store";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import { colorHex } from "@/lib/colors";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (v: boolean) => void;
|
|
project?: Project | null; // editar; vacío = crear
|
|
}
|
|
|
|
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 }: Props) {
|
|
const { add, update, remove } = useProjects();
|
|
const { isGerardo } = useAuth();
|
|
const isEdit = !!project;
|
|
|
|
const [form, setForm] = useState<Omit<Project, "id" | "createdAt">>(empty());
|
|
|
|
useEffect(() => {
|
|
if (project) {
|
|
const { id: _i, createdAt: _c, ...rest } = project;
|
|
setForm(rest);
|
|
} else if (open) {
|
|
setForm(empty());
|
|
}
|
|
}, [project, open]);
|
|
|
|
const set = <K extends keyof typeof form>(k: K, v: (typeof form)[K]) =>
|
|
setForm((f) => ({ ...f, [k]: v }));
|
|
|
|
// BU → CM automático
|
|
const onBU = (bu: string) => setForm((f) => ({ ...f, bu, cm: BU_CM[bu] ?? "" }));
|
|
|
|
const requiredOk =
|
|
form.nombre.trim() && form.cliente && form.bu && form.marca && form.solicitante.trim();
|
|
|
|
const submit = () => {
|
|
if (!requiredOk) return;
|
|
if (isEdit && project) {
|
|
update(project.id, form);
|
|
} else {
|
|
add({ ...form, id: crypto.randomUUID(), createdAt: Date.now() });
|
|
}
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const onDelete = () => {
|
|
if (project && confirm("¿Eliminar este proyecto?")) {
|
|
remove(project.id);
|
|
onOpenChange(false);
|
|
}
|
|
};
|
|
|
|
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" />
|
|
|
|
{/* Cliente */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Cliente <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select value={form.cliente} onValueChange={(v) => set("cliente", v)}>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="Seleccionar…" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{CLIENTES.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>
|
|
{MARCAS.map((m) => (
|
|
<SelectItem key={m} value={m}>
|
|
{m}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</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>
|
|
{BUS.map((b) => (
|
|
<SelectItem key={b} value={b}>
|
|
{b}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* CM autocompletado */}
|
|
<div className="space-y-1.5">
|
|
<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="—"
|
|
/>
|
|
</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. Pegá 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 (solo Marrero edita) */}
|
|
<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>
|
|
<Select
|
|
value={form.status || "_none"}
|
|
onValueChange={(v) => set("status", (v === "_none" ? "" : v) as Status | "")}
|
|
disabled={!isGerardo}
|
|
>
|
|
<SelectTrigger className={cn("h-10", !isGerardo && "opacity-70")}>
|
|
<SelectValue placeholder="Sin estatus" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="_none">Sin estatus</SelectItem>
|
|
{STATUS.map((s) => (
|
|
<SelectItem key={s} value={s}>
|
|
{s}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{!isGerardo && (
|
|
<p className="text-[11px] text-muted-foreground">
|
|
Solo Marrero puede cambiar el estatus.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Monto: SOLO visible para 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 lo ves vos.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1.5">
|
|
<Label className="text-muted-foreground flex items-center gap-1.5">
|
|
<Lock className="w-3.5 h-3.5" /> Interno Cargado
|
|
</Label>
|
|
<div className="h-10 px-3 flex items-center rounded-md border border-dashed border-border bg-muted/30 text-xs text-muted-foreground italic">
|
|
Solo visible para Marrero
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="px-7 py-4 border-t bg-muted/30 rounded-b-lg flex-row justify-between sm:justify-between">
|
|
<div>
|
|
{isEdit && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onDelete}
|
|
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-1.5" /> Eliminar
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={submit} disabled={!requiredOk}>
|
|
{isEdit ? "Guardar cambios" : "Crear proyecto"}
|
|
</Button>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|