chore: subir version inicial del Tablero CDC
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { COLORS, type ColorId } from "@/data/lists";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ColorPicker({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: ColorId;
|
||||
onChange: (c: ColorId) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{COLORS.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => onChange(c.id)}
|
||||
title={c.name}
|
||||
className={cn(
|
||||
"w-9 h-9 rounded-full flex items-center justify-center transition-all ring-offset-2 ring-offset-background hover:scale-110",
|
||||
value === c.id && "ring-2 ring-foreground/40 scale-110",
|
||||
)}
|
||||
style={{ backgroundColor: c.hex }}
|
||||
>
|
||||
{value === c.id && <Check className="w-4 h-4 text-white drop-shadow" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { Link as LinkIcon, ExternalLink, Plus, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
prefix?: string; // ej "AF"
|
||||
links: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
readOnly?: boolean;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
export function LinkList({ label, prefix, links, onChange, readOnly, emptyText }: Props) {
|
||||
const [draft, setDraft] = useState("");
|
||||
|
||||
const add = () => {
|
||||
const v = draft.trim();
|
||||
if (!v) return;
|
||||
onChange([...links, v]);
|
||||
setDraft("");
|
||||
};
|
||||
const update = (i: number, v: string) => onChange(links.map((x, idx) => (idx === i ? v : x)));
|
||||
const remove = (i: number) => onChange(links.filter((_, idx) => idx !== i));
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{label}{" "}
|
||||
{prefix && <span className="text-xs text-muted-foreground font-normal">({prefix})</span>}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{links.length}</span>
|
||||
</div>
|
||||
|
||||
{links.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground italic px-1">{emptyText ?? "Sin links aún"}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{links.map((l, i) => (
|
||||
<div key={i} className="group flex items-center gap-1.5">
|
||||
<div className="flex-shrink-0 w-7 h-7 rounded-md bg-accent flex items-center justify-center text-[10px] font-semibold text-accent-foreground">
|
||||
{prefix ?? "·"}
|
||||
{i + 1}
|
||||
</div>
|
||||
{readOnly ? (
|
||||
<a
|
||||
href={l}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex-1 text-sm text-primary hover:underline truncate flex items-center gap-1.5 py-1.5 px-2 rounded-md hover:bg-accent/60 transition-colors"
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{l}</span>
|
||||
<ExternalLink className="w-3 h-3 flex-shrink-0 opacity-60" />
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
value={l}
|
||||
onChange={(e) => update(i, e.target.value)}
|
||||
placeholder="https://…"
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!readOnly && (
|
||||
<div className="flex gap-1.5 pt-1">
|
||||
<Input
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), add())}
|
||||
placeholder="Pegar link y Enter"
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm" onClick={add} className="h-9">
|
||||
<Plus className="w-4 h-4 mr-1" /> Añadir
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { MapPin, User, Tag, Link2, FileCheck2, CheckCircle2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
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 }) {
|
||||
const closed = !!project.status;
|
||||
const { isGerardo } = useAuth();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"group text-left w-full rounded-2xl bg-card overflow-hidden border border-border",
|
||||
"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",
|
||||
)}
|
||||
style={{
|
||||
boxShadow: closed ? "0 1px 2px oklch(0.5 0.02 280 / 0.04)" : "var(--shadow-soft)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!closed) e.currentTarget.style.boxShadow = "var(--shadow-hover)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.boxShadow = closed
|
||||
? "0 1px 2px oklch(0.5 0.02 280 / 0.04)"
|
||||
: "var(--shadow-soft)";
|
||||
}}
|
||||
>
|
||||
{/* Pestaña de color con título */}
|
||||
<div
|
||||
className="px-4 py-3"
|
||||
style={{
|
||||
backgroundColor: closed ? "#9AA0A6" : colorHex(project.color),
|
||||
}}
|
||||
>
|
||||
<h3 className="text-white font-semibold text-sm leading-snug line-clamp-2 drop-shadow-sm">
|
||||
{project.nombre || "Sin título"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Cuerpo blanco */}
|
||||
<div className="p-4 space-y-2.5">
|
||||
<Row icon={<Tag className="w-3.5 h-3.5" />} label="Cliente" value={project.cliente} />
|
||||
<Row icon={<Tag className="w-3.5 h-3.5" />} label="Marca" value={project.marca} />
|
||||
<Row icon={<MapPin className="w-3.5 h-3.5" />} label="País" value={project.bu} />
|
||||
<Row icon={<User className="w-3.5 h-3.5" />} label="Solicita" value={project.solicitante} />
|
||||
|
||||
<div className="flex items-center justify-between pt-2 mt-2 border-t border-border/70">
|
||||
<div className="flex items-center gap-3 text-[11px] text-muted-foreground">
|
||||
{project.propuestaLinks.length > 0 && (
|
||||
<span className="flex items-center gap-1" title="Propuestas">
|
||||
<Link2 className="w-3 h-3" /> {project.propuestaLinks.length}
|
||||
</span>
|
||||
)}
|
||||
{project.afLinks.length > 0 && (
|
||||
<span className="flex items-center gap-1" title="Artes finales">
|
||||
<FileCheck2 className="w-3 h-3" /> AF·{project.afLinks.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isGerardo && project.monto != null && (
|
||||
<span className="text-[11px] font-medium text-foreground/70">
|
||||
${project.monto.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{project.status && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"text-[10px] px-1.5 py-0 h-5 font-medium",
|
||||
project.status === "Aprobado" &&
|
||||
"bg-emerald-100 text-emerald-800 hover:bg-emerald-100",
|
||||
project.status === "No aprobado" && "bg-red-100 text-red-800 hover:bg-red-100",
|
||||
)}
|
||||
>
|
||||
{project.status === "Aprobado" && <CheckCircle2 className="w-2.5 h-2.5 mr-0.5" />}
|
||||
{project.status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-start gap-2 text-xs">
|
||||
<span className="text-muted-foreground mt-0.5 flex-shrink-0">{icon}</span>
|
||||
<span className="text-muted-foreground w-14 flex-shrink-0">{label}</span>
|
||||
<span className="text-foreground font-medium truncate">{value || "—"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user