"use client" import { useState } from "react" 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Plus, Search, Eye, Edit, Share2, FileDown, Trash2 } from "lucide-react" import { useAuth } from "@/lib/auth-context" import Link from "next/link" import { createPortal } from "react-dom" import { toJpeg } from "html-to-image" import { jsPDF } from "jspdf" import { RecetaDocumento } from "@/components/receta-documento" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { apiFetch, getErrorMessage } from "@/lib/apiutils" import { useEffect } from "react" import { PaginationControl } from "@/components/ui/pagination-control" interface Producto { nombre: string dosis: string unidad: string } interface Receta { id: string nombre: string productor: string establecimiento: string lote: string cultivo: string hectareas: number fechaCreacion: string creador: string productos: Producto[] observaciones: string estado?: string raw?: any } // ... imports import { useApi } from "@/lib/use-api" import { useTableSort } from "@/lib/use-table-sort" import { toast } from "sonner" export default function RecetasPage() { const { user } = useAuth() const { sortBy, sortDir, handleSort, appendSortParams, SortIcon } = useTableSort("fecha", "desc") const [currentPage, setCurrentPage] = useState(1) // Use SWR for fetching with pagination const [searchTerm, setSearchTerm] = useState("") const [filterCultivo, setFilterCultivo] = useState("") const [selectedRecetaForPdf, setSelectedRecetaForPdf] = useState<{ receta: any, action: 'descarga' | 'compartir' } | null>(null) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [recetaToDelete, setRecetaToDelete] = useState(null) const handleDeleteReceta = async () => { if (!recetaToDelete) return try { const res = await apiFetch(`/recetas/${recetaToDelete}`, { method: "DELETE", }) if (res.ok) { toast.success("Receta eliminada exitosamente") await mutate() } else { const msg = await getErrorMessage(res) toast.error(msg) } } catch (e) { console.error(e) toast.error("Error al eliminar la receta") } finally { setIsDeleteDialogOpen(false) setRecetaToDelete(null) } } useEffect(() => { if (selectedRecetaForPdf) { const { receta, action } = selectedRecetaForPdf // Esperamos un instante para asegurarnos de que el portal oculto se renderizó en el DOM setTimeout(async () => { const elementId = `hidden-pdf-container-${receta.id}` const element = document.getElementById(elementId) if (!element) return const filename = `Receta-${receta.numero || receta.id}.pdf` try { toast.loading("Generando archivo PDF...", { id: "pdfToast" }) let dataUrl = "" try { dataUrl = await toJpeg(element, { quality: 0.95, pixelRatio: 2 }) } catch (internalErr) { console.warn("html-to-image falló (posible bug en Firefox), reintentando sin cargar tipografías externas", internalErr) dataUrl = await toJpeg(element, { quality: 0.95, pixelRatio: 2, fontEmbedCSS: '', skipFonts: true }) } const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }) const imgProps = pdf.getImageProperties(dataUrl) const pdfPageWidth = pdf.internal.pageSize.getWidth() const pdfPageHeight = pdf.internal.pageSize.getHeight() let finalWidth = pdfPageWidth let finalHeight = (imgProps.height * finalWidth) / imgProps.width if (finalHeight > pdfPageHeight) { finalHeight = pdfPageHeight finalWidth = (imgProps.width * finalHeight) / imgProps.height } const marginLeft = (pdfPageWidth - finalWidth) / 2 pdf.addImage(dataUrl, 'JPEG', marginLeft, 0, finalWidth, finalHeight) const blob = pdf.output('blob') const file = new File([blob], filename, { type: 'application/pdf' }) toast.dismiss("pdfToast") if (action === 'compartir' && navigator.canShare && navigator.canShare({ files: [file] })) { try { await navigator.share({ files: [file], title: filename, text: `Te envío la receta ${filename} adjunta.` }) toast.success("Abriendo menú de compartir...") } catch (err) { console.log("El usuario canceló el Share Sheet", err) } } else { const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) toast.success("El PDF ha sido descargado.") } } catch(e) { console.error(e) toast.dismiss("pdfToast") toast.error("Error crítico al procesar el archivo.") } finally { setSelectedRecetaForPdf(null) } }, 100) } }, [selectedRecetaForPdf]) // Debounce search term could be good, but for now direct binding or simple timeout // Building query string const queryParams = new URLSearchParams() queryParams.append("page", String(currentPage)) if (searchTerm) queryParams.append("search", searchTerm) if (filterCultivo) queryParams.append("cultivo", filterCultivo) appendSortParams(queryParams) const { data: rawData, isLoading, mutate } = useApi(`/recetas?${queryParams.toString()}`) // Fetch Cultivos for dynamic filter and management const { data: cultivosRaw, mutate: mutateCultivos } = useApi("/cultivos") const cultivosList = Array.isArray(cultivosRaw) ? cultivosRaw : (cultivosRaw?.data || []) const [isCultivoDialogOpen, setIsCultivoDialogOpen] = useState(false) const [newCultivoName, setNewCultivoName] = useState("") const handleCreateCultivo = async () => { if (!newCultivoName) return try { const res = await apiFetch("/cultivos", { method: "POST", body: JSON.stringify({ nombre: newCultivoName }) }) if (res.ok) { await mutateCultivos() setNewCultivoName("") setIsCultivoDialogOpen(false) // Optionally alert success? } else { const msg = await getErrorMessage(res) toast.error(msg) } } catch (e) { console.error(e) } } const dataList = rawData?.data || [] const lastPage = rawData?.last_page || 1 const total = rawData?.total || 0 // Map data const recetas: Receta[] = dataList.map((r: any) => ({ id: String(r.id), nombre: r.numero || `Receta #${r.id}`, productor: r.productor?.nombre || "N/A", establecimiento: r.establecimiento?.nombre || "N/A", lote: r.lote?.nombre || "N/A", cultivo: r.cultivo || (r.cultivoRel ? r.cultivoRel.nombre : "N/A"), // Handle both legacy string and relation hectareas: Number(r.superficie), fechaCreacion: r.fecha, creador: r.ingeniero_responsable, observaciones: r.observaciones, estado: r.estado || "borrador", productos: [], raw: r })) const filteredRecetas = recetas // No client-side filtering anymore return (

Gestión de Recetas

Crea y administra recetas agronómicas para tus clientes

Total Recetas {isLoading ? (
) : (
{rawData?.stats?.total || 0}
)}
Lista de Recetas Administra todas las recetas agronómicas creadas
{ setSearchTerm(e.target.value) setCurrentPage(1) // Reset to page 1 on search }} className="pl-10 w-40 sm:w-64" />
Nuevo Cultivo Agrega un nuevo cultivo al sistema.
setNewCultivoName(e.target.value)} placeholder="Ej: Soja" />
handleSort("numero")}>
Nro Receta
handleSort("productor")}>
Productor
handleSort("establecimiento")}>
Establecimiento
Lote Cultivo handleSort("superficie")}>
Hectáreas
handleSort("fecha")}>
Fecha
Acciones
{isLoading && recetas.length === 0 ? ( Cargando recetas... ) : filteredRecetas.length === 0 ? ( No se encontraron recetas ) : ( filteredRecetas.map((receta) => ( {receta.nombre} {receta.productor} {receta.establecimiento} {receta.lote} {receta.cultivo} {receta.hectareas} ha {receta.fechaCreacion}
)) )}
{selectedRecetaForPdf && createPortal(
, document.body )} Eliminar Receta ¿Está seguro que desea eliminar esta receta? Esta acción no se puede deshacer. ) }