| import { useEffect, useState } from "react"; |
| import BrandLoader from "./BrandLoader"; |
| import { fetchPurchaseOrders } from "../lib/api"; |
| import type { PurchaseOrder } from "../lib/types"; |
| import { Language, translations } from "../lib/translations"; |
|
|
| type Props = { |
| lang: Language; |
| }; |
|
|
| export default function MarketMonitor({ lang }: Props) { |
| const t = translations[lang]; |
| 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); |
| }, [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 { |
| |
| 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">{t.marketMonitorTitle}</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"> |
| {t.monitoringOrders.replace('today', '').trim()} <span className="text-white font-bold">{ocs.length.toLocaleString()}</span> {lang === 'es' ? 'órdenes activas de hoy.' : '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" ? t.liveStream : f === "aceptada" ? t.accepted : t.sentToVendor} |
| </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">{t.orderIdDesc}</th> |
| <th className="px-6 py-5 hidden md:table-cell">{t.buyer}</th> |
| <th className="px-6 py-5 hidden lg:table-cell">{t.vendor}</th> |
| <th className="px-4 sm:px-6 py-5 text-right">{t.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"> |
| {lang === 'es' ? 'Mostrando' : 'Showing'} {((page - 1) * itemsPerPage) + 1} {lang === 'es' ? 'a' : 'to'} {Math.min(page * itemsPerPage, ocs.length)} {lang === 'es' ? 'de' : '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" |
| > |
| {lang === 'es' ? 'Anterior' : '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" |
| > |
| {lang === 'es' ? 'Siguiente' : '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> |
| ); |
| } |
|
|