Álvaro Valenzuela Valdes commited on
Commit
42c0d3f
·
1 Parent(s): 38054b7

feat: refactor TenderSearch for build stability with sub-renders

Browse files
Files changed (1) hide show
  1. frontend/components/TenderSearch.tsx +210 -13
frontend/components/TenderSearch.tsx CHANGED
@@ -1,29 +1,226 @@
1
  "use client";
2
 
3
- import { useState } from "react";
 
4
  import type { Tender } from "../lib/types";
5
- import { Language } from "../lib/translations";
 
6
  import type { CompanyProfile } from "../lib/types";
7
 
8
  type Props = {
9
  tenders: Tender[];
10
- onSearch: (params: any) => void;
11
  onAnalyze: (tender: Tender) => void;
 
 
12
  lang: Language;
13
  companyProfile: CompanyProfile;
14
  };
15
 
16
- export default function TenderSearch({ tenders, onSearch, onAnalyze, lang, companyProfile }: Props) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  return (
18
- <div className="p-20 text-center">
19
- <h1 className="text-4xl font-black text-white">RECOVERY MODE</h1>
20
- <p className="text-slate-400 mt-4">The search system is being recalibrated to resolve a compilation issue.</p>
21
- <button
22
- onClick={() => window.location.reload()}
23
- className="mt-8 px-8 py-4 bg-purple-600 text-white rounded-2xl font-bold"
24
- >
25
- Reload Platform
26
- </button>
27
  </div>
28
  );
29
  }
 
1
  "use client";
2
 
3
+ import { useMemo, useState, useRef, useEffect } from "react";
4
+ import BrandLoader from "./BrandLoader";
5
  import type { Tender } from "../lib/types";
6
+ import { Language, translations } from "../lib/translations";
7
+ import AgentChat from "./AgentChat";
8
  import type { CompanyProfile } from "../lib/types";
9
 
10
  type Props = {
11
  tenders: Tender[];
12
+ onSearch: (params: { keyword?: string; buyer?: string; provider_code?: string; org_code?: string; status?: string; code?: string; date?: string; type_code?: string; skip?: number; limit?: number; isAgile?: boolean }) => void;
13
  onAnalyze: (tender: Tender) => void;
14
+ forceShowFollowed?: boolean;
15
+ initialKeyword?: string;
16
  lang: Language;
17
  companyProfile: CompanyProfile;
18
  };
19
 
20
+ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "", lang, companyProfile }: Props) {
21
+ const t = translations[lang];
22
+ const [keyword, setKeyword] = useState(initialKeyword);
23
+ const [buyerCode, setBuyerCode] = useState("");
24
+ const [providerCode, setProviderCode] = useState("");
25
+ const [orgCode, setOrgCode] = useState("");
26
+ const [status, setStatus] = useState("");
27
+ const [date, setDate] = useState("");
28
+ const [typeCode, setTypeCode] = useState("");
29
+ const [showAdvanced, setShowAdvanced] = useState(false);
30
+ const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null);
31
+ const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
32
+ const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
33
+ const [activeDetailTab, setActiveDetailTab] = useState<"Overview" | "Agent Chat">("Overview");
34
+
35
+ const [followedTenders, setFollowedTenders] = useState<Tender[]>(() => {
36
+ if (typeof window !== 'undefined') {
37
+ const saved = localStorage.getItem('andes_followed_tenders_full');
38
+ return saved ? JSON.parse(saved) : [];
39
+ }
40
+ return [];
41
+ });
42
+
43
+ const followedCodes = useMemo(() => followedTenders.map(item => item.code), [followedTenders]);
44
+ const [showOnlyFollowed, setShowOnlyFollowed] = useState(forceShowFollowed);
45
+ const [isLoading, setIsLoading] = useState(false);
46
+ const [isAgileMode, setIsAgileMode] = useState(false);
47
+ const isSearchPending = useRef(false);
48
+
49
+ const filteredTenders = useMemo(() => {
50
+ if (showOnlyFollowed) return followedTenders;
51
+ let list = tenders;
52
+ if (isAgileMode) {
53
+ list = list.filter(item =>
54
+ item.code.includes('COT26') ||
55
+ item.name.toLowerCase().includes('compra ágil') ||
56
+ item.sector?.toLowerCase().includes('agil')
57
+ );
58
+ }
59
+ return list;
60
+ }, [tenders, showOnlyFollowed, followedTenders, isAgileMode]);
61
+
62
+ useEffect(() => {
63
+ if (forceShowFollowed) setShowOnlyFollowed(true);
64
+ }, [forceShowFollowed]);
65
+
66
+ useEffect(() => {
67
+ localStorage.setItem('andes_followed_tenders_full', JSON.stringify(followedTenders));
68
+ }, [followedTenders]);
69
+
70
+ const toggleFollow = (tender: Tender) => {
71
+ setFollowedTenders(prev => {
72
+ const isFollowing = prev.some(item => item.code === tender.code);
73
+ return isFollowing ? prev.filter(item => item.code !== tender.code) : [...prev, tender];
74
+ });
75
+ };
76
+
77
+ const handleSearch = async (e?: React.FormEvent) => {
78
+ if (e) e.preventDefault();
79
+ if (isSearchPending.current) return;
80
+ isSearchPending.current = true;
81
+ setIsLoading(true);
82
+ try {
83
+ const isCode = /^[0-9]+-[0-9]+-[A-Z0-9]+$/i.test(keyword);
84
+ await onSearch({
85
+ keyword: isCode ? undefined : keyword,
86
+ code: isCode ? keyword : undefined,
87
+ org_code: orgCode || undefined,
88
+ status: status || undefined,
89
+ type_code: typeCode || undefined,
90
+ date,
91
+ skip: 0,
92
+ limit: 50,
93
+ isAgile: isAgileMode
94
+ });
95
+ } catch (error) {
96
+ console.error(error);
97
+ } finally {
98
+ setIsLoading(false);
99
+ isSearchPending.current = false;
100
+ }
101
+ };
102
+
103
+ const isTenderCode = /^[0-9]+-[0-9]+-[A-Z0-9]+$/i.test(keyword);
104
+ const isLiveSearch = Boolean(isTenderCode || orgCode || status || date || typeCode);
105
+ const searchButtonLabel = isLoading ? "Searching..." : isLiveSearch ? "Live MP Search" : "Fetch Active Tenders";
106
+
107
+ // VIEW: Search & List
108
+ const renderListView = () => (
109
+ <div className="space-y-8">
110
+ <div className={`glass-card rounded-3xl p-8 mb-4 border transition-all duration-500 ${forceShowFollowed ? 'border-purple-500/30 bg-purple-500/5 shadow-[0_0_50px_rgba(168,85,247,0.1)]' : 'border-white/10'}`}>
111
+ <div className="mb-6 flex justify-between items-start">
112
+ <div>
113
+ <div className="flex items-center gap-3 mb-2">
114
+ <div className={`w-10 h-10 rounded-2xl flex items-center justify-center text-xl ${forceShowFollowed ? 'bg-purple-500 text-white' : 'bg-white/5 text-slate-400'}`}>
115
+ {forceShowFollowed ? "★" : "📡"}
116
+ </div>
117
+ <h2 className="text-3xl font-black text-white tracking-tight">{forceShowFollowed ? "My Portfolio" : "Tender Discovery"}</h2>
118
+ </div>
119
+ <p className="text-slate-400 text-sm">Real-time access to the Chilean public procurement market.</p>
120
+ </div>
121
+ </div>
122
+
123
+ {!forceShowFollowed && (
124
+ <form onSubmit={handleSearch} className="relative z-10 space-y-6">
125
+ <div className="flex flex-col md:flex-row gap-4">
126
+ <div className="relative flex-1">
127
+ <input
128
+ type="text"
129
+ placeholder="Search by name, ID, or description..."
130
+ className="w-full bg-slate-900/60 border border-white/10 rounded-2xl pl-6 pr-4 py-4 text-white focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all"
131
+ value={keyword}
132
+ onChange={(e) => setKeyword(e.target.value)}
133
+ />
134
+ </div>
135
+ <div className="flex gap-2">
136
+ <button type="button" onClick={() => setShowAdvanced(!showAdvanced)} className={`px-6 py-4 rounded-2xl border font-bold text-sm transition-all ${showAdvanced ? 'bg-purple-500/20 border-purple-500/50 text-purple-300' : 'bg-white/5 border-white/10 text-slate-400'}`}>Settings</button>
137
+ <button type="submit" disabled={isLoading} className="px-8 py-4 bg-purple-600 hover:bg-purple-500 text-white rounded-2xl font-bold transition-all shadow-lg active:scale-95">{searchButtonLabel}</button>
138
+ </div>
139
+ </div>
140
+
141
+ {showAdvanced && (
142
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-6 glass-card rounded-3xl bg-white/[0.02] border border-white/5">
143
+ <div className="space-y-1"><label className="text-[10px] font-bold uppercase text-slate-500 ml-1">Date</label><input type="date" className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2 text-white text-sm [color-scheme:dark]" value={date} onChange={(e) => setDate(e.target.value)} /></div>
144
+ <div className="space-y-1"><label className="text-[10px] font-bold uppercase text-slate-500 ml-1">Org ID</label><input type="text" placeholder="e.g. 6945" className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2 text-white text-sm" value={orgCode} onChange={(e) => setOrgCode(e.target.value)} /></div>
145
+ <div className="space-y-1"><label className="text-[10px] font-bold uppercase text-slate-500 ml-1">Status</label><select className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2 text-white text-sm" value={status} onChange={(e) => setStatus(e.target.value)}><option value="">All</option><option value="5">Publicada</option><option value="7">Desierta</option></select></div>
146
+ <div className="space-y-1"><label className="text-[10px] font-bold uppercase text-slate-500 ml-1">Type</label><select className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2 text-white text-sm" value={typeCode} onChange={(e) => setTypeCode(e.target.value)}><option value="">All</option><option value="LE">LE</option><option value="LP">LP</option></select></div>
147
+ </div>
148
+ )}
149
+ </form>
150
+ )}
151
+ </div>
152
+
153
+ <div className="glass-card rounded-3xl overflow-hidden border border-white/5">
154
+ <table className="w-full text-left text-sm table-fixed">
155
+ <thead className="bg-white/5 text-slate-500 uppercase text-[10px] font-bold border-b border-white/5">
156
+ <tr>
157
+ <th className="px-6 py-5 w-[100px]">ID</th>
158
+ <th className="px-6 py-5 w-[300px]">Opportunity</th>
159
+ <th className="px-6 py-5 w-[200px]">Buyer</th>
160
+ <th className="px-6 py-5 text-center w-[120px]">Status</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody className="divide-y divide-white/5">
164
+ {filteredTenders.map((item) => (
165
+ <tr key={item.code} className="hover:bg-white/[0.04] transition-colors group cursor-pointer" onClick={() => setSelectedTenderForModal(item)}>
166
+ <td className="px-6 py-5 font-mono text-purple-400 text-[10px]">{item.code}</td>
167
+ <td className="px-6 py-5"><div className="font-bold text-white truncate text-xs">{item.name}</div></td>
168
+ <td className="px-6 py-5 text-slate-400 text-[11px] truncate">{item.buyer}</td>
169
+ <td className="px-6 py-5 text-center">
170
+ <span className={`inline-block rounded-full px-3 py-1 text-[9px] font-black uppercase ${item.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400' : 'bg-slate-800 text-slate-500'}`}>{item.status}</span>
171
+ </td>
172
+ </tr>
173
+ ))}
174
+ </tbody>
175
+ </table>
176
+ </div>
177
+ </div>
178
+ );
179
+
180
+ // VIEW: Detail Modal
181
+ const renderDetailView = (tender: Tender) => (
182
+ <div className="animate-in slide-in-from-right-8 fade-in duration-700 w-full max-w-[1600px] mx-auto pt-4 pb-20">
183
+ <div className="flex justify-between items-end mb-8">
184
+ <button onClick={() => setSelectedTenderForModal(null)} className="flex items-center gap-2 text-slate-400 hover:text-white transition group">
185
+ <span className="text-xl group-hover:-translate-x-1 transition-transform">←</span>
186
+ <span className="text-sm font-bold uppercase tracking-widest">Back to search</span>
187
+ </button>
188
+ <div className="flex bg-white/5 p-1 rounded-2xl border border-white/10">
189
+ <button onClick={() => setActiveDetailTab("Overview")} className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Overview" ? "bg-purple-600 text-white" : "text-slate-500"}`}>Overview</button>
190
+ <button onClick={() => setActiveDetailTab("Agent Chat")} className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Agent Chat" ? "bg-purple-600 text-white" : "text-slate-500"}`}>Agent Chat</button>
191
+ </div>
192
+ </div>
193
+
194
+ {activeDetailTab === "Overview" ? (
195
+ <div className="glass-card rounded-[2.5rem] overflow-hidden border border-white/5 bg-slate-900/40 backdrop-blur-xl p-10 md:p-14">
196
+ <div className="flex items-center gap-3 mb-6">
197
+ <span className="text-sm font-mono text-purple-400 bg-purple-400/10 px-3 py-1 rounded-lg">{tender.code}</span>
198
+ <span className="px-3 py-1 rounded-lg text-xs font-black uppercase bg-green-500/10 text-green-400">{tender.status}</span>
199
+ </div>
200
+ <h3 className="text-3xl md:text-4xl font-black text-white leading-tight mb-8">{tender.name}</h3>
201
+ <div className="grid lg:grid-cols-3 gap-12">
202
+ <div className="lg:col-span-2 space-y-8">
203
+ <div className="text-slate-300 leading-relaxed text-lg bg-white/[0.02] p-8 rounded-[2rem] border border-white/5 whitespace-pre-wrap">{tender.description || "No description provided."}</div>
204
+ </div>
205
+ <div className="space-y-6">
206
+ <div className="p-8 rounded-[2rem] bg-purple-600/10 border border-purple-500/20">
207
+ <div className="text-[10px] text-slate-500 font-bold uppercase mb-1">Closing Deadline</div>
208
+ <div className="text-2xl font-black text-white font-mono">{tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}</div>
209
+ </div>
210
+ <button onClick={() => { onAnalyze(tender); setSelectedTenderForModal(null); }} className="w-full premium-gradient text-white px-8 py-4 rounded-2xl font-black uppercase shadow-xl">Analyze with AI</button>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ ) : (
215
+ <AgentChat tender={tender} companyProfile={companyProfile} />
216
+ )}
217
+ </div>
218
+ );
219
+
220
  return (
221
+ <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
222
+ {selectedTenderForModal ? renderDetailView(selectedTenderForModal) : renderListView()}
223
+ {isLoading && <BrandLoader />}
 
 
 
 
 
 
224
  </div>
225
  );
226
  }