Álvaro Valenzuela Valdes
Optimize mobile UI, fix layout overlaps and add tender questions link
bd7895c | "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { fetchPurchaseOrders } from "../lib/api"; | |
| import { PurchaseOrder } from "../lib/types"; | |
| import BrandLoader from "./BrandLoader"; | |
| export default function MarketMonitor() { | |
| const [ocs, setOcs] = useState<PurchaseOrder[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [filter, setFilter] = useState("todos"); | |
| const [page, setPage] = useState(1); | |
| const itemsPerPage = 50; | |
| useEffect(() => { | |
| loadOcs(); | |
| setPage(1); // Reset page on filter change | |
| }, [filter]); | |
| async function loadOcs() { | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| const data = await fetchPurchaseOrders(undefined, filter); | |
| if (!data || data.length === 0) { | |
| setError("No purchase orders found for today. Try again later or check your API connection."); | |
| setOcs([]); | |
| } else { | |
| // Sort by code descending (usually higher codes are newer) | |
| const sorted = [...data].sort((a, b) => b.code.localeCompare(a.code)); | |
| setOcs(sorted); | |
| } | |
| } catch (e) { | |
| const errorMsg = e instanceof Error ? e.message : "Failed to load purchase orders. Check your backend connection."; | |
| console.error("OC Load Error:", e); | |
| setError(errorMsg); | |
| setOcs([]); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| } | |
| const formatCurrency = (amount: number | null, currency: string | null) => { | |
| if (!amount || amount === 0) return <span className="text-slate-600 italic">Pending...</span>; | |
| return new Intl.NumberFormat("es-CL", { | |
| style: "currency", | |
| currency: currency || "CLP", | |
| maximumFractionDigits: 0 | |
| }).format(amount); | |
| }; | |
| const paginatedOcs = ocs.slice((page - 1) * itemsPerPage, page * itemsPerPage); | |
| const totalPages = Math.ceil(ocs.length / itemsPerPage); | |
| return ( | |
| <div className="space-y-8 animate-in fade-in duration-700"> | |
| <div className="flex flex-col md:flex-row md:items-center justify-between gap-6"> | |
| <div> | |
| <p className="text-[10px] uppercase tracking-[0.4em] text-cyan/60 font-black mb-2">Real-Time Intelligence</p> | |
| <h2 className="text-4xl font-black text-white tracking-tight">Market Monitor</h2> | |
| <div className="flex items-center gap-3 mt-2"> | |
| <span className="flex h-2 w-2 rounded-full bg-green-500 animate-pulse" /> | |
| <p className="text-slate-400 text-sm"> | |
| Monitoring <span className="text-white font-bold">{ocs.length.toLocaleString()}</span> active orders from today. | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex bg-slate-900/50 p-1 rounded-2xl border border-white/5 backdrop-blur-xl"> | |
| {["todos", "aceptada", "enviadaproveedor"].map((f) => ( | |
| <button | |
| key={f} | |
| onClick={() => setFilter(f)} | |
| className={`px-6 py-2.5 rounded-xl text-[10px] uppercase font-black tracking-widest transition-all ${ | |
| filter === f ? "bg-cyan text-slate-950 shadow-lg shadow-cyan/20" : "text-slate-500 hover:text-white" | |
| }`} | |
| > | |
| {f === "todos" ? "Live Stream" : f} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="grid gap-6"> | |
| {isLoading ? ( | |
| <div className="py-20"> | |
| <BrandLoader /> | |
| </div> | |
| ) : error ? ( | |
| <div className="glass-card rounded-[2rem] p-8 border border-red-500/20 bg-red-500/5"> | |
| <div className="flex items-start gap-4"> | |
| <div className="text-2xl">⚠️</div> | |
| <div className="flex-1"> | |
| <h3 className="text-white font-bold mb-2">Connection Error</h3> | |
| <p className="text-slate-300 text-sm mb-4">{error}</p> | |
| <div className="flex gap-3"> | |
| <button | |
| onClick={loadOcs} | |
| className="px-6 py-2 bg-cyan text-slate-950 font-bold rounded-lg hover:bg-cyan/90 transition-all" | |
| > | |
| 🔄 Retry | |
| </button> | |
| <a | |
| href="#" | |
| className="px-6 py-2 bg-white/5 border border-white/10 text-white font-bold rounded-lg hover:bg-white/10 transition-all" | |
| > | |
| Troubleshoot | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ocs.length > 0 ? ( | |
| <> | |
| <div className="glass-card rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl shadow-black/50"> | |
| <div className="overflow-x-auto custom-scrollbar max-h-[600px]"> | |
| <table className="w-full text-left text-xs border-collapse sticky-header"> | |
| <thead className="sticky top-0 z-10"> | |
| <tr className="bg-slate-900/95 backdrop-blur-md text-slate-500 uppercase font-black tracking-tighter border-b border-white/5"> | |
| <th className="px-4 sm:px-6 py-5">Order ID / Description</th> | |
| <th className="px-6 py-5 hidden md:table-cell">Buyer</th> | |
| <th className="px-6 py-5 hidden lg:table-cell">Vendor</th> | |
| <th className="px-4 sm:px-6 py-5 text-right">Total</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {paginatedOcs.map((oc) => ( | |
| <tr key={oc.code} className="hover:bg-white/[0.03] transition-colors group"> | |
| <td className="px-4 sm:px-6 py-5 max-w-md"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <span className="text-cyan font-bold font-mono text-[9px] bg-cyan/5 px-2 py-0.5 rounded border border-cyan/10"> | |
| {oc.code} | |
| </span> | |
| </div> | |
| <div className="text-white font-bold line-clamp-1 group-hover:line-clamp-none transition-all cursor-help text-xs sm:text-sm" title={oc.name}> | |
| {oc.name || "Orden de Compra"} | |
| </div> | |
| <div className="md:hidden text-[10px] text-slate-500 mt-1 truncate max-w-[200px]"> | |
| {oc.buyer} | |
| </div> | |
| </td> | |
| <td className="px-6 py-5 hidden md:table-cell"> | |
| <div className="text-slate-300 font-medium truncate max-w-[150px] text-[11px]"> | |
| {oc.buyer !== "Unknown" ? oc.buyer : <span className="opacity-30">...</span>} | |
| </div> | |
| </td> | |
| <td className="px-6 py-5 hidden lg:table-cell"> | |
| <div className="text-sky-400 font-bold truncate max-w-[150px] text-[11px]"> | |
| {oc.provider !== "Unknown" ? oc.provider : <span className="opacity-30">...</span>} | |
| </div> | |
| </td> | |
| <td className="px-4 sm:px-6 py-5 text-right"> | |
| <div className="text-white font-black text-xs sm:text-sm"> | |
| {formatCurrency(oc.total_amount, oc.currency)} | |
| </div> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/* Pagination Controls */} | |
| <div className="flex items-center justify-between px-4"> | |
| <div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest"> | |
| Showing {((page - 1) * itemsPerPage) + 1} to {Math.min(page * itemsPerPage, ocs.length)} of {ocs.length} | |
| </div> | |
| <div className="flex gap-2"> | |
| <button | |
| disabled={page === 1} | |
| onClick={() => setPage(p => p - 1)} | |
| className="px-4 py-2 rounded-lg bg-white/5 border border-white/10 text-xs font-bold disabled:opacity-30 hover:bg-white/10" | |
| > | |
| Previous | |
| </button> | |
| <button | |
| disabled={page === totalPages} | |
| onClick={() => setPage(p => p + 1)} | |
| className="px-4 py-2 rounded-lg bg-white/5 border border-white/10 text-xs font-bold disabled:opacity-30 hover:bg-white/10" | |
| > | |
| Next | |
| </button> | |
| </div> | |
| </div> | |
| </> | |
| ) : ( | |
| <div className="py-40 text-center glass-card rounded-[2rem] border border-white/5"> | |
| <div className="text-4xl mb-4">🛒</div> | |
| <p className="text-slate-500 font-medium italic">No purchase orders detected in the last hour.</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |