Álvaro Valenzuela Valdes commited on
Commit
a66ed25
·
1 Parent(s): 06b757a

feat: add pagination and improve data display for Market Monitor

Browse files
Files changed (1) hide show
  1. frontend/components/MarketMonitor.tsx +84 -62
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
- setOcs(data);
 
 
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 === null) return "---";
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
- <p className="text-slate-400 text-sm mt-2 max-w-xl">
45
- Monitor real-time Purchase Orders (OCs) issued by public organizations.
46
- Identify high-velocity buying patterns and competitive wins.
47
- </p>
 
 
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
- <div className="glass-card rounded-[2rem] overflow-hidden border border-white/5">
72
- <div className="overflow-x-auto custom-scrollbar">
73
- <table className="w-full text-left text-xs border-collapse">
74
- <thead>
75
- <tr className="bg-white/5 text-slate-500 uppercase font-black tracking-tighter border-b border-white/5">
76
- <th className="px-6 py-5">Issue Date</th>
77
- <th className="px-6 py-5">Order ID / Description</th>
78
- <th className="px-6 py-5">Buyer Organism</th>
79
- <th className="px-6 py-5">Vendor (Winner)</th>
80
- <th className="px-6 py-5 text-right">Total Amount</th>
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
- </tbody>
126
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </div>
128
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>