Álvaro Valenzuela Valdes commited on
Commit ·
63cdff5
1
Parent(s): 48f7497
feat: persistent search history and professional PDF export with branded headers
Browse files- backend/app/routers/analysis.py +13 -0
- backend/app/schemas/analysis.py +6 -0
- frontend/app/page.tsx +17 -1
- frontend/components/AgentAnalysis.tsx +12 -1
- frontend/components/AnalysisHistory.tsx +68 -3
- frontend/globals.css +69 -0
- frontend/lib/api.ts +19 -0
backend/app/routers/analysis.py
CHANGED
|
@@ -12,6 +12,7 @@ router = APIRouter()
|
|
| 12 |
|
| 13 |
# Load initial history from disk
|
| 14 |
analysis_history: List[AnalysisRecord] = load_from_json(AnalysisRecord, "analysis_history.json")
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
@router.post("/analyze", response_model=AnalysisResult)
|
|
@@ -62,3 +63,15 @@ async def agent_chat(request: ChatRequest):
|
|
| 62 |
if not response:
|
| 63 |
response = "Lo siento, tuve un problema procesando tu solicitud. ¿Podrías intentar de nuevo?"
|
| 64 |
return {"response": response}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# Load initial history from disk
|
| 14 |
analysis_history: List[AnalysisRecord] = load_from_json(AnalysisRecord, "analysis_history.json")
|
| 15 |
+
search_history: List[SearchRecord] = load_from_json(SearchRecord, "search_history.json")
|
| 16 |
|
| 17 |
|
| 18 |
@router.post("/analyze", response_model=AnalysisResult)
|
|
|
|
| 63 |
if not response:
|
| 64 |
response = "Lo siento, tuve un problema procesando tu solicitud. ¿Podrías intentar de nuevo?"
|
| 65 |
return {"response": response}
|
| 66 |
+
|
| 67 |
+
@router.post("/search-history")
|
| 68 |
+
def save_search_history(record: SearchRecord):
|
| 69 |
+
search_history.insert(0, record)
|
| 70 |
+
if len(search_history) > 50:
|
| 71 |
+
search_history.pop()
|
| 72 |
+
save_to_json(search_history, "search_history.json")
|
| 73 |
+
return {"status": "ok"}
|
| 74 |
+
|
| 75 |
+
@router.get("/search-history", response_model=List[SearchRecord])
|
| 76 |
+
def get_search_history():
|
| 77 |
+
return search_history
|
backend/app/schemas/analysis.py
CHANGED
|
@@ -67,3 +67,9 @@ class AnalysisRecord(BaseModel):
|
|
| 67 |
tender_name: str
|
| 68 |
analyzed_at: datetime
|
| 69 |
analysis: AnalysisResult
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
tender_name: str
|
| 68 |
analyzed_at: datetime
|
| 69 |
analysis: AnalysisResult
|
| 70 |
+
|
| 71 |
+
class SearchRecord(BaseModel):
|
| 72 |
+
query: str
|
| 73 |
+
results_count: int
|
| 74 |
+
searched_at: datetime
|
| 75 |
+
is_agile: bool = False
|
frontend/app/page.tsx
CHANGED
|
@@ -47,6 +47,7 @@ export default function HomePage() {
|
|
| 47 |
});
|
| 48 |
const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
|
| 49 |
const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
|
|
|
|
| 50 |
const [status, setStatus] = useState("listening");
|
| 51 |
const [searchKeyword, setSearchKeyword] = useState("");
|
| 52 |
const [lang, setLang] = useState<Language>("en");
|
|
@@ -123,6 +124,14 @@ export default function HomePage() {
|
|
| 123 |
console.error("History load error", e);
|
| 124 |
}
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
try {
|
| 127 |
const initialTenders = await searchTenders({ status: 'activas' });
|
| 128 |
setTenders(initialTenders);
|
|
@@ -157,6 +166,13 @@ export default function HomePage() {
|
|
| 157 |
results = await searchTenders(params);
|
| 158 |
}
|
| 159 |
setTenders(results);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
} catch (error) {
|
| 161 |
console.error("Search error:", error);
|
| 162 |
}
|
|
@@ -306,7 +322,7 @@ export default function HomePage() {
|
|
| 306 |
/>
|
| 307 |
)}
|
| 308 |
{activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
|
| 309 |
-
{activeTab === "History" && <AnalysisHistory history={analysisHistory} />}
|
| 310 |
{activeTab === "About" && <SystemInfo />}
|
| 311 |
</div>
|
| 312 |
</main>
|
|
|
|
| 47 |
});
|
| 48 |
const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
|
| 49 |
const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
|
| 50 |
+
const [searchHistory, setSearchHistory] = useState<any[]>([]);
|
| 51 |
const [status, setStatus] = useState("listening");
|
| 52 |
const [searchKeyword, setSearchKeyword] = useState("");
|
| 53 |
const [lang, setLang] = useState<Language>("en");
|
|
|
|
| 124 |
console.error("History load error", e);
|
| 125 |
}
|
| 126 |
|
| 127 |
+
try {
|
| 128 |
+
const { fetchSearchHistory } = await import("../lib/api");
|
| 129 |
+
const sHistory = await fetchSearchHistory();
|
| 130 |
+
setSearchHistory(sHistory);
|
| 131 |
+
} catch (e) {
|
| 132 |
+
console.error("Search history load error", e);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
try {
|
| 136 |
const initialTenders = await searchTenders({ status: 'activas' });
|
| 137 |
setTenders(initialTenders);
|
|
|
|
| 166 |
results = await searchTenders(params);
|
| 167 |
}
|
| 168 |
setTenders(results);
|
| 169 |
+
// Log search to history
|
| 170 |
+
if (params.keyword || params.code) {
|
| 171 |
+
const { saveSearchHistory, fetchSearchHistory } = await import("../lib/api");
|
| 172 |
+
await saveSearchHistory(params.keyword || params.code || "Active Tenders", results.length, params.isAgile);
|
| 173 |
+
const sHistory = await fetchSearchHistory();
|
| 174 |
+
setSearchHistory(sHistory);
|
| 175 |
+
}
|
| 176 |
} catch (error) {
|
| 177 |
console.error("Search error:", error);
|
| 178 |
}
|
|
|
|
| 322 |
/>
|
| 323 |
)}
|
| 324 |
{activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
|
| 325 |
+
{activeTab === "History" && <AnalysisHistory history={analysisHistory} searchHistory={searchHistory} />}
|
| 326 |
{activeTab === "About" && <SystemInfo />}
|
| 327 |
</div>
|
| 328 |
</main>
|
frontend/components/AgentAnalysis.tsx
CHANGED
|
@@ -565,7 +565,18 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 565 |
<div id="analysis-results" className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-500 scroll-mt-20">
|
| 566 |
<div className="lg:col-span-8 space-y-8">
|
| 567 |
<div className="glass-card rounded-3xl p-10 bg-white/[0.02]">
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
<div>
|
| 570 |
<div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
|
| 571 |
<h3 className="text-6xl font-black text-white">{activeAnalysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
|
|
|
|
| 565 |
<div id="analysis-results" className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-500 scroll-mt-20">
|
| 566 |
<div className="lg:col-span-8 space-y-8">
|
| 567 |
<div className="glass-card rounded-3xl p-10 bg-white/[0.02]">
|
| 568 |
+
+ {/* Professional Print Header */}
|
| 569 |
+
+ <div className="hidden print-only mb-12 border-b-4 border-slate-900 pb-8 text-center">
|
| 570 |
+
+ <h1 className="text-4xl font-black text-slate-900 mb-2">ANDESOPS AI</h1>
|
| 571 |
+
+ <p className="text-sm font-bold uppercase tracking-[0.5em] text-slate-500">Intelligent Bidding Analysis Report</p>
|
| 572 |
+
+ <div className="mt-6 flex justify-between text-[10px] font-mono text-slate-400">
|
| 573 |
+
+ <span>DATE: {new Date().toLocaleDateString()}</span>
|
| 574 |
+
+ <span>REF ID: {tender?.code}</span>
|
| 575 |
+
+ <span>CONFIDENTIAL - FOR INTERNAL USE ONLY</span>
|
| 576 |
+
+ </div>
|
| 577 |
+
+ </div>
|
| 578 |
+
+
|
| 579 |
+
<div className="flex items-start justify-between mb-8">
|
| 580 |
<div>
|
| 581 |
<div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
|
| 582 |
<h3 className="text-6xl font-black text-white">{activeAnalysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
|
frontend/components/AnalysisHistory.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import type { AnalysisHistoryItem } from "../lib/types";
|
|
| 3 |
|
| 4 |
type Props = {
|
| 5 |
history: AnalysisHistoryItem[];
|
|
|
|
| 6 |
};
|
| 7 |
|
| 8 |
export default function AnalysisHistory({ history }: Props) {
|
|
@@ -14,6 +15,8 @@ export default function AnalysisHistory({ history }: Props) {
|
|
| 14 |
);
|
| 15 |
};
|
| 16 |
|
|
|
|
|
|
|
| 17 |
if (!history.length) {
|
| 18 |
return (
|
| 19 |
<div className="flex flex-col items-center justify-center min-h-[40vh] glass-card rounded-3xl border border-white/5 p-12 text-center animate-in fade-in duration-700">
|
|
@@ -26,7 +29,25 @@ export default function AnalysisHistory({ history }: Props) {
|
|
| 26 |
|
| 27 |
return (
|
| 28 |
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
| 29 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
{history.map((item) => {
|
| 31 |
const itemId = `${item.tender_code}-${item.analyzed_at}`;
|
| 32 |
const isExpanded = expandedItems.includes(itemId);
|
|
@@ -98,7 +119,18 @@ export default function AnalysisHistory({ history }: Props) {
|
|
| 98 |
|
| 99 |
{isExpanded && (
|
| 100 |
<div className="mt-8 p-6 rounded-3xl bg-black/40 border border-white/5 animate-in slide-in-from-top-4 duration-500">
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
{item.analysis.raw_responses && (
|
| 104 |
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
@@ -132,7 +164,40 @@ export default function AnalysisHistory({ history }: Props) {
|
|
| 132 |
</div>
|
| 133 |
);
|
| 134 |
})}
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
</div>
|
| 137 |
);
|
| 138 |
}
|
|
|
|
| 3 |
|
| 4 |
type Props = {
|
| 5 |
history: AnalysisHistoryItem[];
|
| 6 |
+
searchHistory?: any[];
|
| 7 |
};
|
| 8 |
|
| 9 |
export default function AnalysisHistory({ history }: Props) {
|
|
|
|
| 15 |
);
|
| 16 |
};
|
| 17 |
|
| 18 |
+
const [activeHistoryTab, setActiveHistoryTab] = useState<"Analysis" | "Searches">("Analysis");
|
| 19 |
+
|
| 20 |
if (!history.length) {
|
| 21 |
return (
|
| 22 |
<div className="flex flex-col items-center justify-center min-h-[40vh] glass-card rounded-3xl border border-white/5 p-12 text-center animate-in fade-in duration-700">
|
|
|
|
| 29 |
|
| 30 |
return (
|
| 31 |
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
| 32 |
+
<div className="flex items-center justify-between mb-4">
|
| 33 |
+
<div className="flex bg-white/5 p-1 rounded-2xl border border-white/10">
|
| 34 |
+
<button
|
| 35 |
+
onClick={() => setActiveHistoryTab("Analysis")}
|
| 36 |
+
className={`px-6 py-2 rounded-xl text-xs font-black uppercase transition-all ${activeHistoryTab === "Analysis" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}
|
| 37 |
+
>
|
| 38 |
+
Analysis Logs
|
| 39 |
+
</button>
|
| 40 |
+
<button
|
| 41 |
+
onClick={() => setActiveHistoryTab("Searches")}
|
| 42 |
+
className={`px-6 py-2 rounded-xl text-xs font-black uppercase transition-all ${activeHistoryTab === "Searches" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}
|
| 43 |
+
>
|
| 44 |
+
Search Audit
|
| 45 |
+
</button>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
{activeHistoryTab === "Analysis" ? (
|
| 50 |
+
<div className="space-y-6">
|
| 51 |
{history.map((item) => {
|
| 52 |
const itemId = `${item.tender_code}-${item.analyzed_at}`;
|
| 53 |
const isExpanded = expandedItems.includes(itemId);
|
|
|
|
| 119 |
|
| 120 |
{isExpanded && (
|
| 121 |
<div className="mt-8 p-6 rounded-3xl bg-black/40 border border-white/5 animate-in slide-in-from-top-4 duration-500">
|
| 122 |
+
{/* Professional Print Header */}
|
| 123 |
+
<div className="hidden print-only mb-12 border-b-4 border-slate-900 pb-8 text-center">
|
| 124 |
+
<h1 className="text-4xl font-black text-slate-900 mb-2">ANDESOPS AI</h1>
|
| 125 |
+
<p className="text-sm font-bold uppercase tracking-[0.5em] text-slate-500">Historical Audit Report</p>
|
| 126 |
+
<div className="mt-6 flex justify-between text-[10px] font-mono text-slate-400">
|
| 127 |
+
<span>DATE: {new Date().toLocaleDateString()}</span>
|
| 128 |
+
<span>REF ID: {item.tender_code}</span>
|
| 129 |
+
<span>ORIGINAL ANALYSIS: {new Date(item.analyzed_at).toLocaleString()}</span>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-4 no-print">Agent Intelligence Log (Full Audit)</h4>
|
| 134 |
|
| 135 |
{item.analysis.raw_responses && (
|
| 136 |
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
|
|
| 164 |
</div>
|
| 165 |
);
|
| 166 |
})}
|
| 167 |
+
</div>
|
| 168 |
+
) : (
|
| 169 |
+
<div className="glass-card rounded-3xl overflow-hidden border border-white/5">
|
| 170 |
+
<table className="w-full text-left text-sm">
|
| 171 |
+
<thead className="bg-white/5 text-slate-500 uppercase text-[10px] font-bold border-b border-white/5">
|
| 172 |
+
<tr>
|
| 173 |
+
<th className="px-6 py-5">Search Query</th>
|
| 174 |
+
<th className="px-6 py-5">Results</th>
|
| 175 |
+
<th className="px-6 py-5">Type</th>
|
| 176 |
+
<th className="px-6 py-5 text-right">Timestamp</th>
|
| 177 |
+
</tr>
|
| 178 |
+
</thead>
|
| 179 |
+
<tbody className="divide-y divide-white/5">
|
| 180 |
+
{searchHistory?.map((s, idx) => (
|
| 181 |
+
<tr key={idx} className="hover:bg-white/[0.04] transition-colors">
|
| 182 |
+
<td className="px-6 py-5 font-bold text-white">{s.query}</td>
|
| 183 |
+
<td className="px-6 py-5 text-cyan font-mono">{s.results_count}</td>
|
| 184 |
+
<td className="px-6 py-5">
|
| 185 |
+
<span className={`px-2 py-0.5 rounded text-[10px] font-black uppercase ${s.is_agile ? 'bg-green-500/10 text-green-400' : 'bg-white/5 text-slate-500'}`}>
|
| 186 |
+
{s.is_agile ? "Agile" : "Standard"}
|
| 187 |
+
</span>
|
| 188 |
+
</td>
|
| 189 |
+
<td className="px-6 py-5 text-right text-slate-500 text-xs">{new Date(s.searched_at).toLocaleString()}</td>
|
| 190 |
+
</tr>
|
| 191 |
+
))}
|
| 192 |
+
{!searchHistory?.length && (
|
| 193 |
+
<tr>
|
| 194 |
+
<td colSpan={4} className="px-6 py-12 text-center text-slate-600 italic">No search logs recorded yet.</td>
|
| 195 |
+
</tr>
|
| 196 |
+
)}
|
| 197 |
+
</tbody>
|
| 198 |
+
</table>
|
| 199 |
+
</div>
|
| 200 |
+
)}
|
| 201 |
</div>
|
| 202 |
);
|
| 203 |
}
|
frontend/globals.css
CHANGED
|
@@ -109,3 +109,72 @@ textarea,
|
|
| 109 |
select {
|
| 110 |
font: inherit;
|
| 111 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
select {
|
| 110 |
font: inherit;
|
| 111 |
}
|
| 112 |
+
/* Professional PDF Print Styles for AndesOps AI reports */
|
| 113 |
+
@media print {
|
| 114 |
+
@page {
|
| 115 |
+
margin: 2cm;
|
| 116 |
+
size: A4;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
body {
|
| 120 |
+
background: white !important;
|
| 121 |
+
color: black !important;
|
| 122 |
+
font-family: 'Inter', system-ui, sans-serif !important;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.glass-card {
|
| 126 |
+
background: white !important;
|
| 127 |
+
border: 1px solid #e2e8f0 !important;
|
| 128 |
+
box-shadow: none !important;
|
| 129 |
+
backdrop-filter: none !important;
|
| 130 |
+
page-break-inside: avoid;
|
| 131 |
+
margin-bottom: 20px !important;
|
| 132 |
+
color: black !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.premium-gradient,
|
| 136 |
+
.bg-purple-600,
|
| 137 |
+
.bg-cyan {
|
| 138 |
+
background: #f8fafc !important;
|
| 139 |
+
color: black !important;
|
| 140 |
+
border: 1px solid #000 !important;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.text-white,
|
| 144 |
+
.text-slate-300,
|
| 145 |
+
.text-slate-400,
|
| 146 |
+
.text-purple-400,
|
| 147 |
+
.text-cyan {
|
| 148 |
+
color: black !important;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/* Hide UI elements */
|
| 152 |
+
nav,
|
| 153 |
+
aside,
|
| 154 |
+
footer,
|
| 155 |
+
button,
|
| 156 |
+
.no-print {
|
| 157 |
+
display: none !important;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* Force display of hidden elements in print */
|
| 161 |
+
.print-only {
|
| 162 |
+
display: block !important;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/* Professional spacing */
|
| 166 |
+
h1, h2, h3, h4 {
|
| 167 |
+
color: #1e293b !important;
|
| 168 |
+
margin-top: 1.5rem !important;
|
| 169 |
+
margin-bottom: 0.75rem !important;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.prose {
|
| 173 |
+
color: #334155 !important;
|
| 174 |
+
line-height: 1.6 !important;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.border-white\/5 {
|
| 178 |
+
border-color: #e2e8f0 !important;
|
| 179 |
+
}
|
| 180 |
+
}
|
frontend/lib/api.ts
CHANGED
|
@@ -167,6 +167,25 @@ export async function fetchAnalysisHistory(): Promise<AnalysisHistoryItem[]> {
|
|
| 167 |
return res.json();
|
| 168 |
}
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
export async function syncDatabase() {
|
| 171 |
const res = await fetch(`${API_BASE}/api/tenders/sync`, { method: "POST" });
|
| 172 |
if (!res.ok) {
|
|
|
|
| 167 |
return res.json();
|
| 168 |
}
|
| 169 |
|
| 170 |
+
export async function saveSearchHistory(query: string, resultsCount: number, isAgile: boolean = false) {
|
| 171 |
+
return fetch(`${API_BASE}/api/search-history`, {
|
| 172 |
+
method: "POST",
|
| 173 |
+
headers: jsonHeaders,
|
| 174 |
+
body: JSON.stringify({
|
| 175 |
+
query,
|
| 176 |
+
results_count: resultsCount,
|
| 177 |
+
searched_at: new Date().toISOString(),
|
| 178 |
+
is_agile: isAgile
|
| 179 |
+
})
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
export async function fetchSearchHistory(): Promise<any[]> {
|
| 184 |
+
const res = await fetch(`${API_BASE}/api/search-history`);
|
| 185 |
+
if (!res.ok) return [];
|
| 186 |
+
return res.json();
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
export async function syncDatabase() {
|
| 190 |
const res = await fetch(`${API_BASE}/api/tenders/sync`, { method: "POST" });
|
| 191 |
if (!res.ok) {
|