Álvaro Valenzuela Valdes
fix: revert portal links to search page to fix permission errors while keeping enhanced backend scraping
7f9e1cc | "use client"; | |
| import { useMemo, useState, useRef, useEffect } from "react"; | |
| import BrandLoader from "./BrandLoader"; | |
| import type { Tender } from "../lib/types"; | |
| import { Language, translations } from "../lib/translations"; | |
| import AgentChat from "./AgentChat"; | |
| import type { CompanyProfile } from "../lib/types"; | |
| type Props = { | |
| tenders: Tender[]; | |
| 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; | |
| onAnalyze: (tender: Tender) => void; | |
| forceShowFollowed?: boolean; | |
| initialKeyword?: string; | |
| lang: Language; | |
| companyProfile: CompanyProfile; | |
| }; | |
| export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "", lang, companyProfile }: Props) { | |
| const t = translations[lang]; | |
| const [keyword, setKeyword] = useState(initialKeyword); | |
| const [buyerCode, setBuyerCode] = useState(""); | |
| const [providerCode, setProviderCode] = useState(""); | |
| const [orgCode, setOrgCode] = useState(""); | |
| const [status, setStatus] = useState(""); | |
| const [date, setDate] = useState(""); | |
| const [typeCode, setTypeCode] = useState(""); | |
| const [showAdvanced, setShowAdvanced] = useState(false); | |
| const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null); | |
| const [selectedCodes, setSelectedCodes] = useState<string[]>([]); | |
| const [isSyncingToAgents, setIsSyncingToAgents] = useState(false); | |
| const [activeDetailTab, setActiveDetailTab] = useState<"Overview" | "Agent Chat">("Overview"); | |
| const [followedTenders, setFollowedTenders] = useState<Tender[]>(() => { | |
| if (typeof window !== 'undefined') { | |
| const saved = localStorage.getItem('andes_followed_tenders_full'); | |
| return saved ? JSON.parse(saved) : []; | |
| } | |
| return []; | |
| }); | |
| const followedCodes = useMemo(() => followedTenders.map(item => item.code), [followedTenders]); | |
| const [showOnlyFollowed, setShowOnlyFollowed] = useState(forceShowFollowed); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [isAgileMode, setIsAgileMode] = useState(false); | |
| const isSearchPending = useRef(false); | |
| const filteredTenders = useMemo(() => { | |
| if (showOnlyFollowed) return followedTenders; | |
| let list = tenders; | |
| if (isAgileMode) { | |
| list = list.filter(item => | |
| item.code.includes('COT26') || | |
| item.name.toLowerCase().includes('compra ágil') || | |
| item.sector?.toLowerCase().includes('agil') | |
| ); | |
| } | |
| return list; | |
| }, [tenders, showOnlyFollowed, followedTenders, isAgileMode]); | |
| useEffect(() => { | |
| if (forceShowFollowed) setShowOnlyFollowed(true); | |
| }, [forceShowFollowed]); | |
| useEffect(() => { | |
| localStorage.setItem('andes_followed_tenders_full', JSON.stringify(followedTenders)); | |
| }, [followedTenders]); | |
| const toggleFollow = (tender: Tender) => { | |
| setFollowedTenders(prev => { | |
| const isFollowing = prev.some(item => item.code === tender.code); | |
| return isFollowing ? prev.filter(item => item.code !== tender.code) : [...prev, tender]; | |
| }); | |
| }; | |
| const handleSearch = async (e?: React.FormEvent) => { | |
| if (e) e.preventDefault(); | |
| if (isSearchPending.current) return; | |
| isSearchPending.current = true; | |
| setIsLoading(true); | |
| try { | |
| const isCode = /^[0-9]+-[0-9]+-[A-Z0-9]+$/i.test(keyword); | |
| const searchParams = { | |
| keyword: isCode ? undefined : keyword, | |
| code: isCode ? keyword : undefined, | |
| org_code: orgCode || undefined, | |
| status: status || undefined, | |
| type_code: typeCode || undefined, | |
| date, | |
| skip: 0, | |
| limit: 50, | |
| isAgile: isAgileMode | |
| }; | |
| console.log("[TenderSearch] Searching with params:", searchParams); | |
| await onSearch(searchParams); | |
| } catch (error) { | |
| console.error("[TenderSearch] Search failed:", error); | |
| const errorMsg = error instanceof Error ? error.message : "Search failed. Check your backend connection."; | |
| alert(`Search Error: ${errorMsg}`); | |
| } finally { | |
| setIsLoading(false); | |
| isSearchPending.current = false; | |
| } | |
| }; | |
| const isTenderCode = /^[0-9]+-[0-9]+-[A-Z0-9]+$/i.test(keyword); | |
| const isLiveSearch = Boolean(isTenderCode || orgCode || status || date || typeCode); | |
| const searchButtonLabel = isLoading ? "Searching..." : isLiveSearch ? "Live MP Search" : "Fetch Active Tenders"; | |
| // VIEW: Search & List | |
| const renderListView = () => ( | |
| <div className="space-y-8"> | |
| <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'}`}> | |
| <div className="mb-6 flex justify-between items-start"> | |
| <div> | |
| <div className="flex items-center gap-3 mb-2"> | |
| <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'}`}> | |
| {forceShowFollowed ? "★" : "📡"} | |
| </div> | |
| <h2 className="text-3xl font-black text-white tracking-tight">{forceShowFollowed ? "My Portfolio" : "Tender Discovery"}</h2> | |
| </div> | |
| <p className="text-slate-400 text-sm">Real-time access to the Chilean public procurement market.</p> | |
| </div> | |
| </div> | |
| {!forceShowFollowed && ( | |
| <form onSubmit={handleSearch} className="relative z-10 space-y-6"> | |
| <div className="flex flex-col md:flex-row gap-4"> | |
| <div className="relative flex-1"> | |
| <input | |
| type="text" | |
| placeholder="Search by name, ID, or description..." | |
| 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" | |
| value={keyword} | |
| onChange={(e) => setKeyword(e.target.value)} | |
| /> | |
| </div> | |
| <div className="flex gap-2"> | |
| <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> | |
| <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> | |
| </div> | |
| </div> | |
| {showAdvanced && ( | |
| <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"> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| </div> | |
| )} | |
| </form> | |
| )} | |
| </div> | |
| <div className="glass-card rounded-3xl overflow-hidden border border-white/5"> | |
| <table className="w-full text-left text-sm table-fixed"> | |
| <thead className="bg-white/5 text-slate-500 uppercase text-[10px] font-bold border-b border-white/5"> | |
| <tr> | |
| <th className="px-4 py-5 w-[50px] text-center"></th> | |
| <th className="px-6 py-5 w-[110px] hidden sm:table-cell">ID</th> | |
| <th className="px-6 py-5 min-w-[200px]">Opportunity</th> | |
| <th className="px-6 py-5 w-[150px] hidden md:table-cell">Buyer</th> | |
| <th className="px-6 py-5 text-center w-[100px] hidden sm:table-cell">Status</th> | |
| <th className="px-6 py-5 w-[80px] text-center">Action</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {filteredTenders.map((item) => ( | |
| <tr key={item.code} className="hover:bg-white/[0.04] transition-colors group"> | |
| <td className="px-2 py-5 text-center"> | |
| <button | |
| onPointerDown={(e) => { | |
| e.stopPropagation(); | |
| toggleFollow(item); | |
| }} | |
| className={`text-lg transition-all hover:scale-125 active:scale-90 p-2 cursor-pointer relative z-50 ${followedCodes.includes(item.code) ? 'text-amber-400 drop-shadow-[0_0_10px_rgba(251,191,36,0.6)]' : 'text-white/30 hover:text-white/60'}`} | |
| > | |
| {followedCodes.includes(item.code) ? "★" : "☆"} | |
| </button> | |
| </td> | |
| <td className="px-6 py-5 font-mono text-purple-400 text-[9px] hidden sm:table-cell">{item.code}</td> | |
| <td className="px-6 py-5"> | |
| <div className="font-bold text-white text-xs sm:text-sm line-clamp-2" title={item.name}>{item.name}</div> | |
| <div className="text-[10px] text-slate-500 mt-1 md:hidden truncate max-w-[150px]">{item.buyer}</div> | |
| </td> | |
| <td className="px-6 py-5 text-slate-400 text-[10px] truncate hidden md:table-cell">{item.buyer}</td> | |
| <td className="px-6 py-5 text-center hidden sm:table-cell"> | |
| <span className={`inline-block rounded-full px-2 py-1 text-[8px] font-black uppercase ${item.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400' : 'bg-slate-800 text-slate-500'}`}>{item.status}</span> | |
| </td> | |
| <td className="px-4 py-5 text-center"> | |
| <button | |
| onClick={() => setSelectedTenderForModal(item)} | |
| className="p-2 sm:px-4 sm:py-2 rounded-xl bg-purple-500/10 border border-purple-500/20 text-[9px] font-black text-purple-400 hover:bg-purple-500 hover:text-white transition-all uppercase tracking-widest" | |
| > | |
| <span className="hidden sm:inline">View</span> | |
| <span className="sm:hidden">→</span> | |
| </button> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| // VIEW: Detail Modal | |
| const renderDetailView = (tender: Tender) => ( | |
| <div className="animate-in slide-in-from-right-8 fade-in duration-700 w-full max-w-[1600px] mx-auto pt-4 pb-20"> | |
| <div className="flex flex-col md:flex-row md:justify-between md:items-end gap-6 mb-8 relative z-20"> | |
| <button | |
| onClick={() => { | |
| console.log("[TenderSearch] Returning to list..."); | |
| setSelectedTenderForModal(null); | |
| }} | |
| className="flex items-center gap-4 px-6 py-3 rounded-2xl bg-white/5 border border-white/10 text-slate-400 hover:text-white hover:bg-white/10 transition-all group active:scale-95 cursor-pointer relative z-30 w-fit" | |
| > | |
| <span className="text-2xl group-hover:-translate-x-1 transition-transform">←</span> | |
| <span className="text-xs font-black uppercase tracking-widest">Back to search</span> | |
| </button> | |
| <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 bg-white/5 p-1 rounded-2xl border border-white/10 w-full md:w-auto"> | |
| <button | |
| onClick={() => toggleFollow(tender)} | |
| className={`px-4 py-2.5 rounded-xl text-[10px] font-black uppercase transition-all flex items-center justify-center gap-2 ${followedCodes.includes(tender.code) ? "bg-amber-500/20 text-amber-400 border border-amber-500/30" : "bg-white/5 text-slate-400 hover:bg-white/10"}`} | |
| > | |
| <span>{followedCodes.includes(tender.code) ? "★" : "☆"}</span> | |
| <span>{followedCodes.includes(tender.code) ? "In Portfolio" : "Add to Portfolio"}</span> | |
| </button> | |
| <div className="hidden sm:block w-px h-6 bg-white/10 mx-1" /> | |
| <div className="flex gap-2"> | |
| <button onClick={() => setActiveDetailTab("Overview")} className={`flex-1 md:flex-none px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Overview" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500 bg-white/5"}`}>Overview</button> | |
| <button onClick={() => setActiveDetailTab("Agent Chat")} className={`flex-1 md:flex-none px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Agent Chat" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500 bg-white/5"}`}>Agent Chat</button> | |
| </div> | |
| </div> | |
| </div> | |
| {activeDetailTab === "Overview" ? ( | |
| <div className="space-y-8"> | |
| <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 relative"> | |
| <div className="absolute top-0 right-0 p-8"> | |
| <a | |
| href={`https://www.mercadopublico.cl/Portal/BuscarLicitacion?texto=${tender.code}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="px-4 py-2 rounded-xl bg-purple-500/10 border border-purple-500/20 text-[10px] font-bold text-purple-400 hover:bg-purple-500/20 transition-all uppercase tracking-widest whitespace-nowrap" | |
| > | |
| Visit Official Site 🔗 | |
| </a> | |
| </div> | |
| <div className="flex items-center gap-3 mb-6"> | |
| <span className="text-sm font-mono text-purple-400 bg-purple-400/10 px-3 py-1 rounded-lg border border-purple-400/20">{tender.code}</span> | |
| <span className="px-3 py-1 rounded-lg text-xs font-black uppercase bg-green-500/10 text-green-400 border border-green-500/20">{tender.status}</span> | |
| <span className="px-3 py-1 rounded-lg text-[10px] font-bold uppercase bg-white/5 text-slate-500 border border-white/10">{tender.type || "N/A"}</span> | |
| </div> | |
| <h3 className="text-3xl md:text-5xl font-black text-white leading-tight mb-4 max-w-4xl">{tender.name}</h3> | |
| <p className="text-slate-400 text-xl font-medium mb-12">{tender.buyer}</p> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12"> | |
| <div className="bg-white/5 p-6 rounded-3xl border border-white/5"> | |
| <p className="text-[10px] uppercase font-bold text-slate-500 tracking-[0.2em] mb-2">Estimated Investment</p> | |
| <p className="text-2xl font-black text-white"> | |
| {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: tender.currency || "CLP", maximumFractionDigits: 0 }).format(tender.estimated_amount) : "N/A"} | |
| </p> | |
| </div> | |
| <div className="bg-white/5 p-6 rounded-3xl border border-white/5"> | |
| <p className="text-[10px] uppercase font-bold text-slate-500 tracking-[0.2em] mb-2">Closing Deadline</p> | |
| <p className="text-2xl font-black text-white font-mono">{tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}</p> | |
| </div> | |
| <div className="bg-white/5 p-6 rounded-3xl border border-white/5"> | |
| <p className="text-[10px] uppercase font-bold text-slate-500 tracking-[0.2em] mb-2">Region</p> | |
| <p className="text-2xl font-black text-white truncate" title={tender.region}>{tender.region || "Nacional"}</p> | |
| </div> | |
| <div className="bg-white/5 p-6 rounded-3xl border border-white/5"> | |
| <p className="text-[10px] uppercase font-bold text-slate-500 tracking-[0.2em] mb-2">Sector</p> | |
| <p className="text-2xl font-black text-white truncate">{tender.sector || "General"}</p> | |
| </div> | |
| </div> | |
| <div className="grid lg:grid-cols-3 gap-12"> | |
| <div className="lg:col-span-2 space-y-8"> | |
| <div> | |
| <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-4">Detailed Description</h4> | |
| <div className="text-slate-300 leading-relaxed text-lg bg-white/[0.02] p-10 rounded-[2.5rem] border border-white/5 whitespace-pre-wrap">{tender.description || "No description provided."}</div> | |
| </div> | |
| {tender.evaluation_criteria && tender.evaluation_criteria.length > 0 && ( | |
| <div> | |
| <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6 flex items-center gap-2"> | |
| <span className="text-purple-500">⚖️</span> Evaluation Criteria | |
| </h4> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {tender.evaluation_criteria.map((crit, idx) => ( | |
| <div key={idx} className="p-6 rounded-3xl bg-slate-900/60 border border-white/5 hover:border-purple-500/30 transition-all group relative overflow-hidden"> | |
| <div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity"> | |
| <span className="text-4xl font-black">{crit.weight}%</span> | |
| </div> | |
| <div className="flex justify-between items-start mb-2"> | |
| <span className="text-sm font-bold text-slate-200 group-hover:text-purple-400 transition-colors pr-12">{crit.name}</span> | |
| <span className="text-[10px] font-black text-purple-400 bg-purple-400/10 px-2 py-1 rounded-lg border border-purple-400/20">{crit.weight}%</span> | |
| </div> | |
| {crit.description && <p className="text-[11px] text-slate-500 line-clamp-3 italic leading-relaxed">{crit.description}</p>} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Lifecycle Section */} | |
| <div> | |
| <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6 flex items-center gap-2"> | |
| <span className="text-cyan">🔄</span> Procurement Lifecycle | |
| </h4> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| {[ | |
| { label: "Preguntas", icon: "❓", status: "Active" }, | |
| { label: "Historial", icon: "📜", status: "Available" }, | |
| { label: "Apertura", icon: "🔓", status: "Pending" }, | |
| { label: "Adjudicación", icon: "🏆", status: "Future" } | |
| ].map((step, i) => ( | |
| <div key={i} className="p-4 rounded-2xl bg-white/[0.03] border border-white/5 text-center group hover:bg-white/5 transition-all"> | |
| <div className="text-2xl mb-2 group-hover:scale-110 transition">{step.icon}</div> | |
| <div className="text-[10px] font-black uppercase tracking-widest text-slate-300 mb-1">{step.label}</div> | |
| <div className="text-[8px] font-bold text-slate-600 uppercase">{step.status}</div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {tender.items && tender.items.length > 0 && ( | |
| <div> | |
| <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-4">Products / Services Required</h4> | |
| <div className="overflow-hidden rounded-3xl border border-white/5 bg-black/20"> | |
| <table className="w-full text-left text-xs"> | |
| <thead className="bg-white/5 text-slate-500 font-bold uppercase text-[9px]"> | |
| <tr> | |
| <th className="px-6 py-4">Item Name</th> | |
| <th className="px-6 py-4 text-right">Quantity</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {tender.items.map((item, idx) => ( | |
| <tr key={idx} className="hover:bg-white/5 transition-colors"> | |
| <td className="px-6 py-4 text-slate-300 font-medium">{item.name}</td> | |
| <td className="px-6 py-4 text-right font-mono text-cyan">{item.quantity} {item.unit}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-8"> | |
| <div className="p-8 rounded-[2.5rem] bg-purple-600/10 border border-purple-500/20 relative overflow-hidden group"> | |
| <div className="absolute -right-10 -bottom-10 h-32 w-32 bg-purple-500/10 rounded-full blur-3xl group-hover:bg-purple-500/20 transition-all" /> | |
| <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-purple-400 mb-6">Decision Intelligence</h4> | |
| <p className="text-xs text-slate-400 leading-relaxed mb-8">Launch our multi-agent AI pipeline to analyze compliance, risks, and win-probability for this opportunity.</p> | |
| <button onClick={() => { onAnalyze(tender); setSelectedTenderForModal(null); }} className="w-full premium-gradient text-white px-8 py-5 rounded-2xl font-black uppercase shadow-xl shadow-purple-500/20 hover:scale-[1.02] active:scale-[0.98] transition-all">Analyze with AI</button> | |
| </div> | |
| <div className="p-8 rounded-[2.5rem] bg-white/5 border border-white/10"> | |
| <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6">Documents & Attachments</h4> | |
| {tender.attachments && tender.attachments.length > 0 ? ( | |
| <div className="space-y-3"> | |
| {tender.attachments.map((att, idx) => ( | |
| <a key={idx} href={att.url} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between p-5 rounded-3xl bg-black/40 border border-white/5 hover:bg-white/5 hover:border-purple-500/40 transition-all group"> | |
| <div className="flex items-center gap-3 overflow-hidden"> | |
| <span className="text-xl group-hover:scale-110 transition">📄</span> | |
| <span className="text-[11px] text-slate-400 group-hover:text-white truncate max-w-[160px]">{att.name}</span> | |
| </div> | |
| <span className="text-xs opacity-40 group-hover:opacity-100">📥</span> | |
| </a> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="text-center py-10 px-4 rounded-2xl bg-black/20 border border-dashed border-white/10"> | |
| <span className="text-2xl mb-2 block">📄</span> | |
| <p className="text-[10px] text-slate-600 font-bold uppercase leading-relaxed">No direct attachments found. Check the official site for the full bidding package.</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <AgentChat tender={tender} companyProfile={companyProfile} /> | |
| )} | |
| </div> | |
| ); | |
| return ( | |
| <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700"> | |
| {selectedTenderForModal ? renderDetailView(selectedTenderForModal) : renderListView()} | |
| {isLoading && <BrandLoader />} | |
| </div> | |
| ); | |
| } | |