Álvaro Valenzuela Valdes commited on
Commit ·
a66ed25
1
Parent(s): 06b757a
feat: add pagination and improve data display for Market Monitor
Browse files
frontend/components/MarketMonitor.tsx
CHANGED
|
@@ -9,16 +9,21 @@ export default function MarketMonitor() {
|
|
| 9 |
const [ocs, setOcs] = useState<PurchaseOrder[]>([]);
|
| 10 |
const [isLoading, setIsLoading] = useState(true);
|
| 11 |
const [filter, setFilter] = useState("todos");
|
|
|
|
|
|
|
| 12 |
|
| 13 |
useEffect(() => {
|
| 14 |
loadOcs();
|
|
|
|
| 15 |
}, [filter]);
|
| 16 |
|
| 17 |
async function loadOcs() {
|
| 18 |
setIsLoading(true);
|
| 19 |
try {
|
| 20 |
const data = await fetchPurchaseOrders(undefined, filter);
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
} catch (e) {
|
| 23 |
console.error(e);
|
| 24 |
} finally {
|
|
@@ -27,7 +32,7 @@ export default function MarketMonitor() {
|
|
| 27 |
}
|
| 28 |
|
| 29 |
const formatCurrency = (amount: number | null, currency: string | null) => {
|
| 30 |
-
if (amount ===
|
| 31 |
return new Intl.NumberFormat("es-CL", {
|
| 32 |
style: "currency",
|
| 33 |
currency: currency || "CLP",
|
|
@@ -35,16 +40,21 @@ export default function MarketMonitor() {
|
|
| 35 |
}).format(amount);
|
| 36 |
};
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
return (
|
| 39 |
<div className="space-y-8 animate-in fade-in duration-700">
|
| 40 |
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
| 41 |
<div>
|
| 42 |
<p className="text-[10px] uppercase tracking-[0.4em] text-cyan/60 font-black mb-2">Real-Time Intelligence</p>
|
| 43 |
<h2 className="text-4xl font-black text-white tracking-tight">Market Monitor</h2>
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
</div>
|
| 49 |
|
| 50 |
<div className="flex bg-slate-900/50 p-1 rounded-2xl border border-white/5 backdrop-blur-xl">
|
|
@@ -68,64 +78,76 @@ export default function MarketMonitor() {
|
|
| 68 |
<BrandLoader />
|
| 69 |
</div>
|
| 70 |
) : ocs.length > 0 ? (
|
| 71 |
-
<
|
| 72 |
-
<div className="overflow-
|
| 73 |
-
<
|
| 74 |
-
<
|
| 75 |
-
<
|
| 76 |
-
<
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
</tr>
|
| 82 |
-
</thead>
|
| 83 |
-
<tbody className="divide-y divide-white/5">
|
| 84 |
-
{ocs.map((oc) => (
|
| 85 |
-
<tr key={oc.code} className="hover:bg-white/[0.03] transition-colors group">
|
| 86 |
-
<td className="px-6 py-5 whitespace-nowrap">
|
| 87 |
-
<div className="text-slate-300 font-mono">
|
| 88 |
-
{oc.date_creation ? new Date(oc.date_creation).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '---'}
|
| 89 |
-
</div>
|
| 90 |
-
<div className="text-[10px] text-slate-600">
|
| 91 |
-
{oc.date_creation ? new Date(oc.date_creation).toLocaleDateString() : 'Today'}
|
| 92 |
-
</div>
|
| 93 |
-
</td>
|
| 94 |
-
<td className="px-6 py-5 max-w-md">
|
| 95 |
-
<div className="flex items-center gap-2 mb-1">
|
| 96 |
-
<span className="text-cyan font-bold font-mono text-[10px] bg-cyan/5 px-2 py-0.5 rounded border border-cyan/10">
|
| 97 |
-
{oc.code}
|
| 98 |
-
</span>
|
| 99 |
-
{oc.type && (
|
| 100 |
-
<span className="text-[8px] bg-white/5 text-slate-500 px-1.5 py-0.5 rounded uppercase font-black">
|
| 101 |
-
{oc.type}
|
| 102 |
-
</span>
|
| 103 |
-
)}
|
| 104 |
-
</div>
|
| 105 |
-
<div className="text-white font-bold truncate group-hover:text-cyan transition-colors">
|
| 106 |
-
{oc.name || "Orden de Compra"}
|
| 107 |
-
</div>
|
| 108 |
-
</td>
|
| 109 |
-
<td className="px-6 py-5">
|
| 110 |
-
<div className="text-slate-300 font-medium truncate max-w-[200px]">{oc.buyer}</div>
|
| 111 |
-
<div className="text-[10px] text-slate-600">{oc.buyer_rut}</div>
|
| 112 |
-
</td>
|
| 113 |
-
<td className="px-6 py-5">
|
| 114 |
-
<div className="text-sky-400 font-bold truncate max-w-[200px]">{oc.provider}</div>
|
| 115 |
-
<div className="text-[10px] text-slate-600">{oc.provider_rut}</div>
|
| 116 |
-
</td>
|
| 117 |
-
<td className="px-6 py-5 text-right">
|
| 118 |
-
<div className="text-white font-black text-sm">
|
| 119 |
-
{formatCurrency(oc.total_amount, oc.currency)}
|
| 120 |
-
</div>
|
| 121 |
-
<div className="text-[10px] text-slate-500 uppercase font-bold">{oc.currency}</div>
|
| 122 |
-
</td>
|
| 123 |
</tr>
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
</div>
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
) : (
|
| 130 |
<div className="py-40 text-center glass-card rounded-[2rem] border border-white/5">
|
| 131 |
<div className="text-4xl mb-4">🛒</div>
|
|
|
|
| 9 |
const [ocs, setOcs] = useState<PurchaseOrder[]>([]);
|
| 10 |
const [isLoading, setIsLoading] = useState(true);
|
| 11 |
const [filter, setFilter] = useState("todos");
|
| 12 |
+
const [page, setPage] = useState(1);
|
| 13 |
+
const itemsPerPage = 50;
|
| 14 |
|
| 15 |
useEffect(() => {
|
| 16 |
loadOcs();
|
| 17 |
+
setPage(1); // Reset page on filter change
|
| 18 |
}, [filter]);
|
| 19 |
|
| 20 |
async function loadOcs() {
|
| 21 |
setIsLoading(true);
|
| 22 |
try {
|
| 23 |
const data = await fetchPurchaseOrders(undefined, filter);
|
| 24 |
+
// Sort by code descending (usually higher codes are newer)
|
| 25 |
+
const sorted = [...data].sort((a, b) => b.code.localeCompare(a.code));
|
| 26 |
+
setOcs(sorted);
|
| 27 |
} catch (e) {
|
| 28 |
console.error(e);
|
| 29 |
} finally {
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
const formatCurrency = (amount: number | null, currency: string | null) => {
|
| 35 |
+
if (!amount || amount === 0) return <span className="text-slate-600 italic">Pending...</span>;
|
| 36 |
return new Intl.NumberFormat("es-CL", {
|
| 37 |
style: "currency",
|
| 38 |
currency: currency || "CLP",
|
|
|
|
| 40 |
}).format(amount);
|
| 41 |
};
|
| 42 |
|
| 43 |
+
const paginatedOcs = ocs.slice((page - 1) * itemsPerPage, page * itemsPerPage);
|
| 44 |
+
const totalPages = Math.ceil(ocs.length / itemsPerPage);
|
| 45 |
+
|
| 46 |
return (
|
| 47 |
<div className="space-y-8 animate-in fade-in duration-700">
|
| 48 |
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
| 49 |
<div>
|
| 50 |
<p className="text-[10px] uppercase tracking-[0.4em] text-cyan/60 font-black mb-2">Real-Time Intelligence</p>
|
| 51 |
<h2 className="text-4xl font-black text-white tracking-tight">Market Monitor</h2>
|
| 52 |
+
<div className="flex items-center gap-3 mt-2">
|
| 53 |
+
<span className="flex h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
| 54 |
+
<p className="text-slate-400 text-sm">
|
| 55 |
+
Monitoring <span className="text-white font-bold">{ocs.length.toLocaleString()}</span> active orders from today.
|
| 56 |
+
</p>
|
| 57 |
+
</div>
|
| 58 |
</div>
|
| 59 |
|
| 60 |
<div className="flex bg-slate-900/50 p-1 rounded-2xl border border-white/5 backdrop-blur-xl">
|
|
|
|
| 78 |
<BrandLoader />
|
| 79 |
</div>
|
| 80 |
) : ocs.length > 0 ? (
|
| 81 |
+
<>
|
| 82 |
+
<div className="glass-card rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl shadow-black/50">
|
| 83 |
+
<div className="overflow-x-auto custom-scrollbar max-h-[600px]">
|
| 84 |
+
<table className="w-full text-left text-xs border-collapse sticky-header">
|
| 85 |
+
<thead className="sticky top-0 z-10">
|
| 86 |
+
<tr className="bg-slate-900/95 backdrop-blur-md text-slate-500 uppercase font-black tracking-tighter border-b border-white/5">
|
| 87 |
+
<th className="px-6 py-5">Order ID / Description</th>
|
| 88 |
+
<th className="px-6 py-5">Buyer</th>
|
| 89 |
+
<th className="px-6 py-5">Vendor</th>
|
| 90 |
+
<th className="px-6 py-5 text-right">Total Amount</th>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
</tr>
|
| 92 |
+
</thead>
|
| 93 |
+
<tbody className="divide-y divide-white/5">
|
| 94 |
+
{paginatedOcs.map((oc) => (
|
| 95 |
+
<tr key={oc.code} className="hover:bg-white/[0.03] transition-colors group">
|
| 96 |
+
<td className="px-6 py-5 max-w-md">
|
| 97 |
+
<div className="flex items-center gap-2 mb-1">
|
| 98 |
+
<span className="text-cyan font-bold font-mono text-[10px] bg-cyan/5 px-2 py-0.5 rounded border border-cyan/10">
|
| 99 |
+
{oc.code}
|
| 100 |
+
</span>
|
| 101 |
+
</div>
|
| 102 |
+
<div className="text-white font-bold line-clamp-1 group-hover:line-clamp-none transition-all cursor-help" title={oc.name}>
|
| 103 |
+
{oc.name || "Orden de Compra"}
|
| 104 |
+
</div>
|
| 105 |
+
</td>
|
| 106 |
+
<td className="px-6 py-5">
|
| 107 |
+
<div className="text-slate-300 font-medium truncate max-w-[150px]">
|
| 108 |
+
{oc.buyer !== "Unknown" ? oc.buyer : <span className="opacity-30">...</span>}
|
| 109 |
+
</div>
|
| 110 |
+
</td>
|
| 111 |
+
<td className="px-6 py-5">
|
| 112 |
+
<div className="text-sky-400 font-bold truncate max-w-[150px]">
|
| 113 |
+
{oc.provider !== "Unknown" ? oc.provider : <span className="opacity-30">...</span>}
|
| 114 |
+
</div>
|
| 115 |
+
</td>
|
| 116 |
+
<td className="px-6 py-5 text-right">
|
| 117 |
+
<div className="text-white font-black text-sm">
|
| 118 |
+
{formatCurrency(oc.total_amount, oc.currency)}
|
| 119 |
+
</div>
|
| 120 |
+
</td>
|
| 121 |
+
</tr>
|
| 122 |
+
))}
|
| 123 |
+
</tbody>
|
| 124 |
+
</table>
|
| 125 |
+
</div>
|
| 126 |
</div>
|
| 127 |
+
|
| 128 |
+
{/* Pagination Controls */}
|
| 129 |
+
<div className="flex items-center justify-between px-4">
|
| 130 |
+
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">
|
| 131 |
+
Showing {((page - 1) * itemsPerPage) + 1} to {Math.min(page * itemsPerPage, ocs.length)} of {ocs.length}
|
| 132 |
+
</div>
|
| 133 |
+
<div className="flex gap-2">
|
| 134 |
+
<button
|
| 135 |
+
disabled={page === 1}
|
| 136 |
+
onClick={() => setPage(p => p - 1)}
|
| 137 |
+
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"
|
| 138 |
+
>
|
| 139 |
+
Previous
|
| 140 |
+
</button>
|
| 141 |
+
<button
|
| 142 |
+
disabled={page === totalPages}
|
| 143 |
+
onClick={() => setPage(p => p + 1)}
|
| 144 |
+
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"
|
| 145 |
+
>
|
| 146 |
+
Next
|
| 147 |
+
</button>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</>
|
| 151 |
) : (
|
| 152 |
<div className="py-40 text-center glass-card rounded-[2rem] border border-white/5">
|
| 153 |
<div className="text-4xl mb-4">🛒</div>
|