"use client" import { useState, useEffect, useMemo, useRef } from "react" import { useRouter, useSearchParams } from "next/navigation" import { DashboardLayout } from "@/components/dashboard-layout" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Textarea } from "@/components/ui/textarea" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Leaf, Plus, Trash2, Save, Printer, Calendar, MapPin, User, Sprout, ArrowLeft, ChevronDown, Check, Tractor, Pencil, GripVertical } from "lucide-react" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { apiFetch, getErrorMessage } from "@/lib/apiutils" import { useAuth } from "@/lib/auth-context" import { useApi } from "@/lib/use-api" import Link from "next/link" import { toast } from "sonner" import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { restrictToVerticalAxis } from '@dnd-kit/modifiers' interface Productor { id: string nombre: string } interface Establecimiento { id: string nombre: string productorId: string ubicacion: string hectareas: number } interface Insumo { id: string nombreComercial: string principioActivo: string unidadMedida: string categoriaId: string categoriaNombre: string } interface ProductoReceta { id: string insumoId: string nombreProducto: string dosis: number | string unidad: string cantidadTotal: number orden: number } interface RecetaItemState { id: string loteId: string loteNombre: string loteSuperficie: number cultivoId: string cultivoNombre: string labor: string rinde?: string momentoDia: string productos: ProductoReceta[] total_recetas: number total_superficie: number total_establecimientos: number } function SortableProductRow({ producto, index, total, onReorder, children }: any) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: producto.id }); const style = { transform: CSS.Transform.toString(transform), transition, ...(isDragging ? { position: 'relative' as const, zIndex: 99, opacity: 0.8, backgroundColor: 'var(--background)' } : {}) }; const displayOrder = producto.orden > 0 ? producto.orden : index + 1; return (
{ const val = parseInt(e.target.value); if (!isNaN(val)) { const clamped = Math.max(1, Math.min(total, val)); onReorder(producto.id, clamped); } }} className="w-9 h-7 text-center text-sm font-medium rounded-md border bg-muted focus:outline-none focus:ring-1 focus:ring-primary [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" />
{children}
); } export default function EditarRecetaPage() { const router = useRouter() const searchParams = useSearchParams() const recetaId = searchParams.get("id") const { user } = useAuth() const useApiHook = useApi // Use SWR hooks for static data const { data: prodData } = useApiHook("/productores?all=true&status=activo") const productores: Productor[] = useMemo(() => { const raw = prodData?.data || [] return raw.map((p: any) => ({ id: String(p.id), nombre: p.nombre, })) }, [prodData]) const { data: estData } = useApiHook("/establecimientos?all=true&status=activo") const todosEstablecimientos: Establecimiento[] = useMemo(() => { const raw = estData?.data || [] return raw.map((e: any) => ({ id: String(e.id), nombre: e.nombre, productorId: String(e.productor_id), ubicacion: e.ubicacion, hectareas: Number(e.hectareas), })) }, [estData]) const { data: insuData } = useApiHook("/insumos?limit=1000") const insumos: Insumo[] = useMemo(() => { const raw = insuData?.data || [] return raw.map((i: any) => ({ id: String(i.id), nombreComercial: i.nombre_comercial, principioActivo: i.principio_activo, unidadMedida: i.unidad_medida, categoriaId: String(i.categoria_id || ""), categoriaNombre: i.categoria?.nombre || "", })) }, [insuData]) const { data: catData } = useApiHook("/categoria-insumos") const categoriasInsumo: any[] = useMemo(() => { const raw = catData || [] return Array.isArray(raw) ? raw : (raw.data || []) }, [catData]) const { data: cultData } = useApiHook("/cultivos") const rawCultivos = cultData || [] const normalizedCultivos = Array.isArray(rawCultivos) ? rawCultivos : (rawCultivos.data || []) const cultivosList = normalizedCultivos const cultivos = useMemo(() => normalizedCultivos.map((c: any) => ({ id: String(c.id), nombre: c.nombre })), [normalizedCultivos]) const { data: prestData } = useApiHook("/prestadores?status=activo&all=true") const rawPrestadores = Array.isArray(prestData) ? prestData : (prestData?.data || []) const prestadoresList = useMemo(() => Array.isArray(rawPrestadores) ? rawPrestadores : [], [rawPrestadores]) const { data: laboresData } = useApiHook("/labores?status=activo") const defaultLabores = ["Pulverización", "Fertilización", "Siembra", "Barbecho", "Aplicación Foliar", "Cosecha"] const labores: string[] = useMemo(() => { const raw = Array.isArray(laboresData) ? laboresData : (laboresData?.data || []) if (raw && raw.length > 0) { return raw.map((l: any) => l.nombre) } return defaultLabores }, [laboresData]) const [numeroReceta, setNumeroReceta] = useState("") const [fechaActual, setFechaActual] = useState("") const [productorSeleccionado, setProductorSeleccionado] = useState("") const [establecimientoSeleccionado, setEstablecimientoSeleccionado] = useState("") const [prestadorSeleccionado, setPrestadorSeleccionado] = useState("ninguno") // Added state const [establecimientosFiltrados, setEstablecimientosFiltrados] = useState([]) const [cultivo, setCultivo] = useState("") const [lotesSeleccionados, setLotesSeleccionados] = useState([]) const [superficie, setSuperficie] = useState(0) const [filtroCategoria, setFiltroCategoria] = useState>({}) const [labor, setLabor] = useState("") const [rinde, setRinde] = useState("") const [momentoDia, setMomentoDia] = useState("") const [superficieAplicable, setSuperficieAplicable] = useState(0) const [superficieAplicableInput, setSuperficieAplicableInput] = useState("") const isFirstLoadLotes = useRef(true) const [productos, setProductos] = useState([]) const [campanas, setCampanas] = useState([]) const [campanaSeleccionada, setCampanaSeleccionada] = useState("") const [campanasLoading, setCampanasLoading] = useState(false) const [lotes, setLotes] = useState([]) const [lotesLoading, setLotesLoading] = useState(false) const [aplicarConViento, setAplicarConViento] = useState("") const [vientoDireccion, setVientoDireccion] = useState([]) const [vientoIntensidad, setVientoIntensidad] = useState("") const [caudal, setCaudal] = useState("") const [pastillas, setPastillas] = useState("") const [gotas, setGotas] = useState("") const [deriva, setDeriva] = useState("") const [observaciones, setObservaciones] = useState("") const [ingenieroResponsable, setIngenieroResponsable] = useState("") const [matricula, setMatricula] = useState("") const [isLoadingReceta, setIsLoadingReceta] = useState(true) // 1. Cargar Receta Existente useEffect(() => { const fetchReceta = async () => { try { const res = await apiFetch(`/recetas/${recetaId}`) if (res.ok) { const data = await res.json() setNumeroReceta(data.numero || `REC-${data.id}`) // Format date YYYY-MM-DD to DD/MM/YYYY for display if (data.fecha) { const [y, m, d] = data.fecha.split("-") setFechaActual(`${d}/${m}/${y}`) } setProductorSeleccionado(String(data.productor_id)) setPrestadorSeleccionado(data.prestador_id ? String(data.prestador_id) : "ninguno") setEstablecimientoSeleccionado(String(data.establecimiento_id)) // Load Items if (data.items && Array.isArray(data.items)) { const loadedItems: RecetaItemState[] = data.items.map((item: any) => ({ id: String(item.id || Date.now() + Math.random()), loteId: String(item.lote_id), loteNombre: item.lote?.nombre || "Desconocido", loteSuperficie: Number(item.lote?.superficie || 0), cultivoId: String(item.cultivo_id), cultivoNombre: item.cultivo?.nombre || "Desconocido", labor: item.labor, rinde: item.rinde ? String(item.rinde) : undefined, momentoDia: item.momento_dia || "", productos: item.productos ? item.productos.map((p: any, pIdx: number) => ({ id: String(p.id), insumoId: p.insumo_id ? String(p.insumo_id) : "", nombreProducto: p.nombre_producto, dosis: Number(p.dosis), unidad: p.unidad, cantidadTotal: Number(p.cantidad_total), orden: Number(p.orden) > 0 ? Number(p.orden) : pIdx + 1 })) : [] })) setLotesSeleccionados(loadedItems.map(i => i.loteId)) if (loadedItems.length > 0) { setLabor(loadedItems[0].labor) setCultivo(loadedItems[0].cultivoId) setRinde(loadedItems[0].rinde || "") setMomentoDia(loadedItems[0].momentoDia || "") setProductos(loadedItems[0].productos) } else { setLabor("") } } const supVal = Number(data.superficie) setSuperficieAplicable(supVal) setSuperficieAplicableInput(supVal === 0 ? '' : String(supVal)) setCampanaSeleccionada(String(data.campana_id || "")) const hasWindData = data.aplicar_con_viento || !!data.viento_direccion setAplicarConViento(hasWindData ? "SI" : "NO") if (data.viento_direccion) { setVientoDireccion(data.viento_direccion.split(",").map((s: string) => s.trim())) } else { setVientoDireccion([]) } setVientoIntensidad(data.viento_intensidad || "") const cleanCaudal = data.caudal ? String(data.caudal).replace(/\s*l\/ha$/i, "").trim() : "" setCaudal(cleanCaudal) setPastillas(data.pastillas || "") setGotas(data.gotas || "") setDeriva(data.deriva || "") setObservaciones(data.observaciones || "") setIngenieroResponsable(data.ingeniero_responsable) setMatricula(data.matricula) } else { console.error("Error fetching receta") } } catch (e) { console.error(e) } finally { setIsLoadingReceta(false) } } fetchReceta() }, [recetaId]) // Fetch campaigns dependent on establishment useEffect(() => { if (establecimientoSeleccionado) { setCampanasLoading(true) apiFetch(`/campanas`) .then(res => res.json()) .then((data: any) => { setCampanas(data.data || []) }) .catch(err => console.error(err)) .finally(() => setCampanasLoading(false)) } else { setCampanas([]) // setCampanaSeleccionada("") // Avoid resetting if we are loading initial data? } }, [establecimientoSeleccionado]) // Filter Establishments - show all when no producer, filter when one is selected useEffect(() => { if (productorSeleccionado) { const filtrados = todosEstablecimientos.filter((est) => est.productorId === productorSeleccionado) setEstablecimientosFiltrados(filtrados) // Only reset if the current selection is NOT valid for this producer if (establecimientoSeleccionado) { const valid = filtrados.find(e => e.id === establecimientoSeleccionado) if (!valid && !isLoadingReceta) { setEstablecimientoSeleccionado("") setSuperficie(0) } } } else { setEstablecimientosFiltrados(todosEstablecimientos) } }, [productorSeleccionado, todosEstablecimientos, isLoadingReceta]) // Cross-autocomplete: when establishment is selected, auto-fill the producer useEffect(() => { if (establecimientoSeleccionado && !isLoadingReceta) { const est = todosEstablecimientos.find(e => e.id === establecimientoSeleccionado) if (est && est.productorId !== productorSeleccionado) { setProductorSeleccionado(est.productorId) } } }, [establecimientoSeleccionado, todosEstablecimientos, isLoadingReceta]) // Lotes logic useEffect(() => { if (establecimientoSeleccionado) { setLotesLoading(true) apiFetch(`/lotes?establecimiento_id=${establecimientoSeleccionado}`) .then(res => res.json()) .then(data => { if (Array.isArray(data)) { setLotes(data) } else { setLotes([]) } // We don't need to restore lotesSeleccionados here for items logic, // as items store their own loteId. // But we might want to ensure valid loteSeleccionado if user is editing current form. // Only for adding NEW items. }) .catch(err => console.error(err)) .finally(() => setLotesLoading(false)) } else { setLotes([]) } }, [establecimientoSeleccionado, isLoadingReceta]) // Lote Selection Effect for Current Item Form useEffect(() => { if (lotesSeleccionados.length > 0 && lotes.length > 0) { const selectedLotesObj = lotes.filter(l => lotesSeleccionados.includes(String(l.id))) const totalSup = selectedLotesObj.reduce((acc, l) => acc + Number(l.superficie), 0) setSuperficie(totalSup) if (isFirstLoadLotes.current) { isFirstLoadLotes.current = false } else { setSuperficieAplicable(totalSup) } if (selectedLotesObj.length > 0 && selectedLotesObj[0].cultivo_id) { setCultivo(String(selectedLotesObj[0].cultivo_id)) } } else { setSuperficie(0) if (!isFirstLoadLotes.current) { setSuperficieAplicable(0) } } }, [lotesSeleccionados, lotes]) const agregarProducto = () => { // Block if there's already an incomplete row (no product selected or no dose) const hayFilaVacia = productos.some(p => !p.nombreProducto || Number(p.dosis) === 0) if (hayFilaVacia) { toast.warning("Completá el producto y la dosis antes de agregar una nueva fila.") return } const nuevoProducto: ProductoReceta = { id: String(Date.now()), insumoId: "", nombreProducto: "", dosis: 0, unidad: "L/ha", cantidadTotal: 0, orden: productos.length + 1, } setProductos([...productos, nuevoProducto]) } const reorderProducts = (productId: string, newOrden: number) => { setProductos(prev => { const targetProduct = prev.find(p => p.id === productId); if (!targetProduct) return prev; const oldOrden = targetProduct.orden; if (oldOrden === newOrden) return prev; return prev.map(p => { if (p.id === productId) return { ...p, orden: newOrden }; if (oldOrden < newOrden) { if (p.orden > oldOrden && p.orden <= newOrden) return { ...p, orden: p.orden - 1 }; } else { if (p.orden >= newOrden && p.orden < oldOrden) return { ...p, orden: p.orden + 1 }; } return p; }).sort((a, b) => a.orden - b.orden); }); } const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { setProductos((items) => { const oldIndex = items.findIndex((i) => i.id === active.id); const newIndex = items.findIndex((i) => i.id === over.id); const newItems = arrayMove(items, oldIndex, newIndex); return newItems.map((item, index) => ({ ...item, orden: index + 1 })); }); } }; const eliminarProducto = (id: string) => { setProductos(prev => { const filtered = prev.filter((p) => p.id !== id); return filtered.sort((a,b) => a.orden - b.orden).map((p, index) => ({ ...p, orden: index + 1 })); }); } const actualizarProducto = (id: string, campo: keyof ProductoReceta, valor: any) => { setProductos( productos.map((p) => { if (p.id === id) { const productoActualizado = { ...p, [campo]: valor } if (campo === "insumoId" && valor) { const insumo = insumos.find((i) => i.id === valor) if (insumo) { productoActualizado.nombreProducto = insumo.nombreComercial productoActualizado.unidad = `${insumo.unidadMedida}/ha` } } if (campo === "dosis" || (campo === "insumoId" && Number(productoActualizado.dosis) > 0)) { productoActualizado.cantidadTotal = Number((Number(productoActualizado.dosis) * Number(superficieAplicable)).toFixed(3)) } return productoActualizado } return p }), ) } useEffect(() => { setProductos( productos.map((p) => ({ ...p, cantidadTotal: Number((Number(p.dosis) * Number(superficieAplicable)).toFixed(3)), })), ) }, [superficieAplicable]) const handleGuardar = async () => { // Validaciones específicas if (!productorSeleccionado) { toast.error("Por favor seleccione un Productor.") return } if (!establecimientoSeleccionado) { toast.error("Por favor seleccione un Establecimiento.") return } if (lotesSeleccionados.length === 0) { toast.error("Por favor seleccione al menos un Lote afectado.") return } if (!labor) { toast.error("Por favor seleccione una Labor.") return } if (!cultivo) { toast.error("Por favor seleccione el Cultivo.") return } if (labor === "Cosecha" && (!rinde || Number(rinde) <= 0)) { toast.error("Para la labor de Cosecha, debe especificar un rinde mayor a 0.") return } if (labor !== "Cosecha" && productos.length === 0) { toast.error("Debe agregar al menos un producto (o seleccionar Cosecha).") return } if (!ingenieroResponsable) { toast.error("Por favor ingrese el nombre del Ingeniero Responsable.") return } if (!matricula) { toast.error("Por favor ingrese la Matrícula del Ingeniero.") return } try { const [day, month, year] = fechaActual.split("/") const formattedDate = `${year}-${month}-${day}` const payload = { fecha: formattedDate, productor_id: productorSeleccionado, establecimiento_id: establecimientoSeleccionado, campana_id: campanaSeleccionada || null, superficie: Number(superficieAplicable), estado: "borrador", aplicar_con_viento: aplicarConViento === "SI", viento_direccion: vientoDireccion.join(", "), viento_intensidad: vientoIntensidad, caudal: caudal ? `${caudal} l/ha` : null, pastillas, gotas, deriva, observaciones, ingeniero_responsable: ingenieroResponsable, prestador_id: prestadorSeleccionado === "ninguno" ? null : prestadorSeleccionado, matricula, items: lotesSeleccionados.map(loteId => ({ lote_id: loteId, cultivo_id: cultivo, labor: labor, rinde: rinde ? Number(rinde) : null, momento_dia: momentoDia, productos: productos.filter(p => p.nombreProducto).map(p => ({ insumo_id: p.insumoId, nombre_producto: p.nombreProducto, dosis: p.dosis, unidad: p.unidad, cantidad_total: p.cantidadTotal, orden: p.orden })) })) } const res = await apiFetch(`/recetas/${recetaId}`, { method: "PUT", body: JSON.stringify(payload), }) if (res.ok) { toast.success("Receta actualizada exitosamente!") router.push("/dashboard/recetas") } else { const msg = await getErrorMessage(res) toast.error(msg) } } catch (e) { console.error(e) toast.error("Error de conexión") } } const establecimientoData = todosEstablecimientos.find((e) => e.id === establecimientoSeleccionado) if (isLoadingReceta) { return (

Cargando receta...

) } return (

Editar Receta {numeroReceta}

Modificar orden de trabajo

AgroRecetas AgroRecetas
{numeroReceta}

{fechaActual}

{establecimientoSeleccionado && !campanasLoading && campanas.length === 0 && (

No hay campañas activas para este establecimiento.

)}
{establecimientoData && (

{establecimientoData.ubicacion}

)}
Detalle del Lote y Productos Seleccione el lote y formule la mezcla para el mismo
{lotes.map((lote) => ( e.preventDefault()} onCheckedChange={(checked) => { setLotesSeleccionados(prev => checked ? [...prev, String(lote.id)] : prev.filter(id => id !== String(lote.id)) ) }} >
{lote.nombre} {lote.superficie} ha
))}
{ const val = e.target.value; const sanitized = val.replace(',', '.'); if (sanitized === '' || /^\d*\.?\d*$/.test(sanitized)) { setSuperficieAplicableInput(sanitized); } }} onBlur={(e) => { const num = parseFloat(e.target.value); const val = isNaN(num) ? 0 : num; setSuperficieAplicable(val); setSuperficieAplicableInput(val === 0 ? '' : String(val)); }} className="font-mono bg-primary/5 border-primary/20" />
{labor === "Cosecha" && (
{ const val = e.target.value.replace(',', '.'); if (val === '' || /^\d*\.?\d*$/.test(val)) { setRinde(val); } }} />
)}
{/* Products Table for Current Item */} {labor !== "Cosecha" && (

Productos

Productos para esta labor

Orden Producto Dosis Unidad Total p.id)} strategy={verticalListSortingStrategy} > {productos.map((producto, index) => (
{ const val = e.target.value; const sanitized = val.replace(',', '.'); if (sanitized === '' || /^\d*\.?\d*$/.test(sanitized)) { actualizarProducto(producto.id, "dosis", sanitized); } }} onBlur={(e) => { const num = parseFloat(e.target.value); actualizarProducto(producto.id, "dosis", isNaN(num) ? 0 : num); }} className="font-mono" /> {producto.unidad} {Number(producto.cantidadTotal).toFixed(3)}
))}
{superficieAplicable > 0 && (

Nota: Las cantidades totales se calculan automáticamente multiplicando la dosis por la superficie aplicable ({superficieAplicable} ha)

)}
)}
{/* Metadata Card (Conditions, Responsible) */} Condiciones Ambientales y Técnicas Parámetros de aplicación y observaciones
{aplicarConViento === "SI" && (
{["N", "S", "E", "O", "NE", "NO", "SE", "SO"].map((dir) => ( ))} {vientoDireccion.length > 0 && ( ({vientoDireccion.join(", ")}) )}
)} {aplicarConViento === "SI" && (
)}
{ if (e.target.value.length <= 50) setCaudal(e.target.value) }} maxLength={50} placeholder="Ej: 100" type="text" className={caudal.length >= 50 ? 'border-red-500 focus-visible:ring-red-500' : ''} /> {caudal.length >= 50 && (

Has alcanzado el límite máximo de 50 caracteres.

)}
{ if (e.target.value.length <= 50) setPastillas(e.target.value) }} maxLength={50} placeholder="Ej: XR 110-03" type="text" className={pastillas.length >= 50 ? 'border-red-500 focus-visible:ring-red-500' : ''} /> {pastillas.length >= 50 && (

Has alcanzado el límite máximo de 50 caracteres.

)}
{ if (e.target.value.length <= 50) setGotas(e.target.value) }} maxLength={50} placeholder="Ej: 50-70" type="text" className={gotas.length >= 50 ? 'border-red-500 focus-visible:ring-red-500' : ''} /> {gotas.length >= 50 && (

Has alcanzado el límite máximo de 50 caracteres.

)}
{ if (e.target.value.length <= 50) setDeriva(e.target.value) }} maxLength={50} placeholder="Ej: Baja / Media" type="text" className={deriva.length >= 50 ? 'border-red-500 focus-visible:ring-red-500' : ''} /> {deriva.length >= 50 && (

Has alcanzado el límite máximo de 50 caracteres.

)}