脕lvaro Valenzuela Valdes commited on
Commit 路
850319d
1
Parent(s): a81ee34
feat: Multi-language support (EN/ES) and ESG Compliance Monitor
Browse files- frontend/app/page.tsx +32 -9
- frontend/components/Dashboard.tsx +18 -15
- frontend/components/Sidebar.tsx +20 -2
- frontend/components/TenderSearch.tsx +8 -6
- frontend/lib/translations.ts +76 -0
frontend/app/page.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import GlobalSync from "../components/GlobalSync";
|
|
| 13 |
import SystemInfo from "../components/SystemInfo";
|
| 14 |
import { analyzeTender, fetchAnalysisHistory, fetchCompanyProfile, healthCheck, saveCompanyProfile, searchTenders } from "../lib/api";
|
| 15 |
import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile as CompanyProfileType, Tender } from "../lib/types";
|
|
|
|
| 16 |
|
| 17 |
const tabs = [
|
| 18 |
"Dashboard",
|
|
@@ -47,6 +48,10 @@ export default function HomePage() {
|
|
| 47 |
const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
|
| 48 |
const [status, setStatus] = useState("listening");
|
| 49 |
const [searchKeyword, setSearchKeyword] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
// Scroll to top when tab changes
|
| 52 |
useEffect(() => {
|
|
@@ -169,6 +174,7 @@ export default function HomePage() {
|
|
| 169 |
setIsMobileMenuOpen(false);
|
| 170 |
}}
|
| 171 |
status={status}
|
|
|
|
| 172 |
/>
|
| 173 |
</div>
|
| 174 |
</div>
|
|
@@ -186,17 +192,32 @@ export default function HomePage() {
|
|
| 186 |
{activeTab === "My Portfolio" && "Manage your followed opportunities."}
|
| 187 |
</p>
|
| 188 |
</div>
|
| 189 |
-
<div className="hidden md:flex items-center gap-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
</div>
|
| 195 |
-
|
| 196 |
</div>
|
| 197 |
-
<div className="h-
|
| 198 |
-
<button
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
</button>
|
| 201 |
</div>
|
| 202 |
</header>
|
|
@@ -211,6 +232,7 @@ export default function HomePage() {
|
|
| 211 |
reportsGenerated={analysisResult ? 1 : 0}
|
| 212 |
tenders={tenders}
|
| 213 |
onFilterClick={handleFilterClick}
|
|
|
|
| 214 |
/>
|
| 215 |
)}
|
| 216 |
{(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
|
|
@@ -220,6 +242,7 @@ export default function HomePage() {
|
|
| 220 |
onAnalyze={handleTenderSelect}
|
| 221 |
forceShowFollowed={activeTab === "My Portfolio"}
|
| 222 |
initialKeyword={searchKeyword}
|
|
|
|
| 223 |
/>
|
| 224 |
)}
|
| 225 |
{activeTab === "Company Profile" && (
|
|
|
|
| 13 |
import SystemInfo from "../components/SystemInfo";
|
| 14 |
import { analyzeTender, fetchAnalysisHistory, fetchCompanyProfile, healthCheck, saveCompanyProfile, searchTenders } from "../lib/api";
|
| 15 |
import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile as CompanyProfileType, Tender } from "../lib/types";
|
| 16 |
+
import { translations, Language } from "../lib/translations";
|
| 17 |
|
| 18 |
const tabs = [
|
| 19 |
"Dashboard",
|
|
|
|
| 48 |
const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
|
| 49 |
const [status, setStatus] = useState("listening");
|
| 50 |
const [searchKeyword, setSearchKeyword] = useState("");
|
| 51 |
+
const [lang, setLang] = useState<Language>("en");
|
| 52 |
+
|
| 53 |
+
const t = translations[lang];
|
| 54 |
+
|
| 55 |
|
| 56 |
// Scroll to top when tab changes
|
| 57 |
useEffect(() => {
|
|
|
|
| 174 |
setIsMobileMenuOpen(false);
|
| 175 |
}}
|
| 176 |
status={status}
|
| 177 |
+
lang={lang}
|
| 178 |
/>
|
| 179 |
</div>
|
| 180 |
</div>
|
|
|
|
| 192 |
{activeTab === "My Portfolio" && "Manage your followed opportunities."}
|
| 193 |
</p>
|
| 194 |
</div>
|
| 195 |
+
<div className="hidden md:flex items-center gap-6">
|
| 196 |
+
{/* ESG Monitor */}
|
| 197 |
+
<div className="flex flex-col items-end">
|
| 198 |
+
<span className="text-[9px] font-black uppercase tracking-widest text-slate-500 mb-1">{t.esgScore}</span>
|
| 199 |
+
<div className="flex gap-2">
|
| 200 |
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-green-500/10 border border-green-500/20">
|
| 201 |
+
<span className="text-[10px] font-bold text-green-400">E</span>
|
| 202 |
+
<span className="text-[10px] font-mono text-white">92</span>
|
| 203 |
+
</div>
|
| 204 |
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-blue-500/10 border border-blue-500/20">
|
| 205 |
+
<span className="text-[10px] font-bold text-blue-400">S</span>
|
| 206 |
+
<span className="text-[10px] font-mono text-white">85</span>
|
| 207 |
+
</div>
|
| 208 |
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-purple-500/10 border border-purple-500/20">
|
| 209 |
+
<span className="text-[10px] font-bold text-purple-400">G</span>
|
| 210 |
+
<span className="text-[10px] font-mono text-white">96</span>
|
| 211 |
</div>
|
| 212 |
+
</div>
|
| 213 |
</div>
|
| 214 |
+
<div className="h-10 w-px bg-white/10" />
|
| 215 |
+
<button
|
| 216 |
+
onClick={() => setLang(lang === "en" ? "es" : "en")}
|
| 217 |
+
className="bg-white/5 hover:bg-white/10 text-white px-4 py-2 rounded-xl text-xs font-bold border border-white/10 transition-all flex items-center gap-2"
|
| 218 |
+
>
|
| 219 |
+
<span>{lang === "en" ? "馃嚭馃嚫" : "馃嚜馃嚫"}</span>
|
| 220 |
+
<span>{lang === "en" ? "English" : "Espa帽ol"}</span>
|
| 221 |
</button>
|
| 222 |
</div>
|
| 223 |
</header>
|
|
|
|
| 232 |
reportsGenerated={analysisResult ? 1 : 0}
|
| 233 |
tenders={tenders}
|
| 234 |
onFilterClick={handleFilterClick}
|
| 235 |
+
lang={lang}
|
| 236 |
/>
|
| 237 |
)}
|
| 238 |
{(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
|
|
|
|
| 242 |
onAnalyze={handleTenderSelect}
|
| 243 |
forceShowFollowed={activeTab === "My Portfolio"}
|
| 244 |
initialKeyword={searchKeyword}
|
| 245 |
+
lang={lang}
|
| 246 |
/>
|
| 247 |
)}
|
| 248 |
{activeTab === "Company Profile" && (
|
frontend/components/Dashboard.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { useEffect, useMemo, useState } from "react";
|
|
| 4 |
import BrandLoader from "./BrandLoader";
|
| 5 |
import { searchTenders, fetchDbStatus } from "../lib/api";
|
| 6 |
|
|
|
|
|
|
|
| 7 |
type Props = {
|
| 8 |
tendersFound: number;
|
| 9 |
recommendedOpportunities: number;
|
|
@@ -11,6 +13,7 @@ type Props = {
|
|
| 11 |
reportsGenerated: number;
|
| 12 |
tenders: Tender[];
|
| 13 |
onFilterClick?: (type: "sector" | "region", value: string) => void;
|
|
|
|
| 14 |
};
|
| 15 |
|
| 16 |
export default function Dashboard({
|
|
@@ -19,8 +22,10 @@ export default function Dashboard({
|
|
| 19 |
highRiskItems,
|
| 20 |
reportsGenerated,
|
| 21 |
tenders,
|
| 22 |
-
onFilterClick
|
|
|
|
| 23 |
}: Props) {
|
|
|
|
| 24 |
const [isSyncing, setIsSyncing] = useState(false);
|
| 25 |
const [dbStatus, setDbStatus] = useState<any>(null);
|
| 26 |
|
|
@@ -35,11 +40,9 @@ export default function Dashboard({
|
|
| 35 |
const handleGlobalSync = async () => {
|
| 36 |
setIsSyncing(true);
|
| 37 |
try {
|
| 38 |
-
// We trigger a search for "software" or empty to populate initial DB
|
| 39 |
await searchTenders({ keyword: "" });
|
| 40 |
-
// Small delay for the "wow" effect duration
|
| 41 |
await new Promise(r => setTimeout(r, 2500));
|
| 42 |
-
window.location.reload();
|
| 43 |
} catch (e) {
|
| 44 |
console.error(e);
|
| 45 |
} finally {
|
|
@@ -110,33 +113,33 @@ export default function Dashboard({
|
|
| 110 |
{isSyncing && <BrandLoader />}
|
| 111 |
<div className="flex items-center justify-between gap-4">
|
| 112 |
<div>
|
| 113 |
-
<p className="text-sm uppercase tracking-[0.35em] text-cyan/80">
|
| 114 |
<h2 className="mt-3 text-4xl font-semibold text-white">AndesOps AI</h2>
|
| 115 |
<p className="mt-4 max-w-2xl text-slate-300">
|
| 116 |
-
|
| 117 |
</p>
|
| 118 |
</div>
|
| 119 |
<button
|
| 120 |
onClick={handleGlobalSync}
|
| 121 |
className="group relative flex items-center gap-3 overflow-hidden rounded-2xl bg-cyan px-6 py-4 font-bold text-slate-950 transition hover:bg-sky hover:scale-[1.02] active:scale-[0.98]"
|
| 122 |
>
|
| 123 |
-
<span className="relative z-10">
|
| 124 |
<span className="text-xl group-hover:rotate-180 transition-transform duration-700">馃攧</span>
|
| 125 |
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
| 126 |
</button>
|
| 127 |
</div>
|
| 128 |
|
| 129 |
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
| 130 |
-
<StatCard title=
|
| 131 |
-
<StatCard title=
|
| 132 |
-
<StatCard title=
|
| 133 |
-
<StatCard title=
|
| 134 |
</div>
|
| 135 |
|
| 136 |
<div className="grid gap-6 lg:grid-cols-3">
|
| 137 |
{/* Sector Distribution */}
|
| 138 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
|
| 139 |
-
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">
|
| 140 |
<div className="space-y-4">
|
| 141 |
{sectorDistribution.length > 0 ? (
|
| 142 |
sectorDistribution.map(([sector, count]) => (
|
|
@@ -165,7 +168,7 @@ export default function Dashboard({
|
|
| 165 |
|
| 166 |
{/* Region Distribution */}
|
| 167 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
|
| 168 |
-
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">
|
| 169 |
<div className="space-y-4">
|
| 170 |
{regionDistribution.length > 0 ? (
|
| 171 |
regionDistribution.map(([region, count]) => (
|
|
@@ -194,7 +197,7 @@ export default function Dashboard({
|
|
| 194 |
|
| 195 |
{/* Deadline Status */}
|
| 196 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
|
| 197 |
-
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">
|
| 198 |
<div className="space-y-6 pt-2">
|
| 199 |
<div className="flex items-center gap-4">
|
| 200 |
<div className="h-12 w-1.5 bg-red-500 rounded-full" />
|
|
@@ -223,7 +226,7 @@ export default function Dashboard({
|
|
| 223 |
{/* Database Status Table (New) */}
|
| 224 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6 flex flex-col justify-between">
|
| 225 |
<div>
|
| 226 |
-
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">
|
| 227 |
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-900/30">
|
| 228 |
<table className="w-full text-left text-[10px]">
|
| 229 |
<thead className="bg-slate-800/50 text-slate-500 uppercase font-bold">
|
|
|
|
| 4 |
import BrandLoader from "./BrandLoader";
|
| 5 |
import { searchTenders, fetchDbStatus } from "../lib/api";
|
| 6 |
|
| 7 |
+
import { translations, Language } from "../lib/translations";
|
| 8 |
+
|
| 9 |
type Props = {
|
| 10 |
tendersFound: number;
|
| 11 |
recommendedOpportunities: number;
|
|
|
|
| 13 |
reportsGenerated: number;
|
| 14 |
tenders: Tender[];
|
| 15 |
onFilterClick?: (type: "sector" | "region", value: string) => void;
|
| 16 |
+
lang: Language;
|
| 17 |
};
|
| 18 |
|
| 19 |
export default function Dashboard({
|
|
|
|
| 22 |
highRiskItems,
|
| 23 |
reportsGenerated,
|
| 24 |
tenders,
|
| 25 |
+
onFilterClick,
|
| 26 |
+
lang
|
| 27 |
}: Props) {
|
| 28 |
+
const t = translations[lang];
|
| 29 |
const [isSyncing, setIsSyncing] = useState(false);
|
| 30 |
const [dbStatus, setDbStatus] = useState<any>(null);
|
| 31 |
|
|
|
|
| 40 |
const handleGlobalSync = async () => {
|
| 41 |
setIsSyncing(true);
|
| 42 |
try {
|
|
|
|
| 43 |
await searchTenders({ keyword: "" });
|
|
|
|
| 44 |
await new Promise(r => setTimeout(r, 2500));
|
| 45 |
+
window.location.reload();
|
| 46 |
} catch (e) {
|
| 47 |
console.error(e);
|
| 48 |
} finally {
|
|
|
|
| 113 |
{isSyncing && <BrandLoader />}
|
| 114 |
<div className="flex items-center justify-between gap-4">
|
| 115 |
<div>
|
| 116 |
+
<p className="text-sm uppercase tracking-[0.35em] text-cyan/80">{t.resumenEjecutivo}</p>
|
| 117 |
<h2 className="mt-3 text-4xl font-semibold text-white">AndesOps AI</h2>
|
| 118 |
<p className="mt-4 max-w-2xl text-slate-300">
|
| 119 |
+
{t.andesOpsDesc}
|
| 120 |
</p>
|
| 121 |
</div>
|
| 122 |
<button
|
| 123 |
onClick={handleGlobalSync}
|
| 124 |
className="group relative flex items-center gap-3 overflow-hidden rounded-2xl bg-cyan px-6 py-4 font-bold text-slate-950 transition hover:bg-sky hover:scale-[1.02] active:scale-[0.98]"
|
| 125 |
>
|
| 126 |
+
<span className="relative z-10">{t.syncPipeline}</span>
|
| 127 |
<span className="text-xl group-hover:rotate-180 transition-transform duration-700">馃攧</span>
|
| 128 |
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
| 129 |
</button>
|
| 130 |
</div>
|
| 131 |
|
| 132 |
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
| 133 |
+
<StatCard title={t.tendersFound} value={tendersFound} subtitle={t.activeOpps} />
|
| 134 |
+
<StatCard title={t.recommended} value={recommendedOpportunities} subtitle="Fit score > 80%" />
|
| 135 |
+
<StatCard title={t.highRisk} value={highRiskItems} subtitle="Riesgos cr铆ticos" />
|
| 136 |
+
<StatCard title={t.totalPipeline} value={formatAmount(totalAmount)} subtitle="Monto total proyectado" />
|
| 137 |
</div>
|
| 138 |
|
| 139 |
<div className="grid gap-6 lg:grid-cols-3">
|
| 140 |
{/* Sector Distribution */}
|
| 141 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
|
| 142 |
+
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">{t.sectors}</h3>
|
| 143 |
<div className="space-y-4">
|
| 144 |
{sectorDistribution.length > 0 ? (
|
| 145 |
sectorDistribution.map(([sector, count]) => (
|
|
|
|
| 168 |
|
| 169 |
{/* Region Distribution */}
|
| 170 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
|
| 171 |
+
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">{t.regionalDist}</h3>
|
| 172 |
<div className="space-y-4">
|
| 173 |
{regionDistribution.length > 0 ? (
|
| 174 |
regionDistribution.map(([region, count]) => (
|
|
|
|
| 197 |
|
| 198 |
{/* Deadline Status */}
|
| 199 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
|
| 200 |
+
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">{t.deadlines}</h3>
|
| 201 |
<div className="space-y-6 pt-2">
|
| 202 |
<div className="flex items-center gap-4">
|
| 203 |
<div className="h-12 w-1.5 bg-red-500 rounded-full" />
|
|
|
|
| 226 |
{/* Database Status Table (New) */}
|
| 227 |
<div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6 flex flex-col justify-between">
|
| 228 |
<div>
|
| 229 |
+
<h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">{t.integrityMonitor}</h3>
|
| 230 |
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-900/30">
|
| 231 |
<table className="w-full text-left text-[10px]">
|
| 232 |
<thead className="bg-slate-800/50 text-slate-500 uppercase font-bold">
|
frontend/components/Sidebar.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"use client";
|
|
|
|
| 2 |
|
| 3 |
import type { Dispatch, SetStateAction } from "react";
|
| 4 |
|
|
@@ -18,9 +19,26 @@ type Props = {
|
|
| 18 |
activeTab: SidebarTab;
|
| 19 |
onTabSelect: Dispatch<SetStateAction<SidebarTab>>;
|
| 20 |
status: string;
|
|
|
|
| 21 |
};
|
| 22 |
|
| 23 |
-
export default function Sidebar({ tabs, activeTab, onTabSelect, status }: Props) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
return (
|
| 25 |
<aside className="w-72 glass-card rounded-3xl h-[calc(100vh-3rem)] sticky top-6 p-6 flex flex-col gap-8">
|
| 26 |
<div className="flex items-center gap-3 px-2">
|
|
@@ -51,7 +69,7 @@ export default function Sidebar({ tabs, activeTab, onTabSelect, status }: Props)
|
|
| 51 |
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${
|
| 52 |
isActive ? "bg-purple-500 scale-125 shadow-[0_0_8px_rgba(168,85,247,0.8)]" : "bg-transparent group-hover:bg-slate-600"
|
| 53 |
}`} />
|
| 54 |
-
<span className="font-medium text-sm">{tab}</span>
|
| 55 |
</button>
|
| 56 |
);
|
| 57 |
})}
|
|
|
|
| 1 |
"use client";
|
| 2 |
+
import { translations, Language } from "../lib/translations";
|
| 3 |
|
| 4 |
import type { Dispatch, SetStateAction } from "react";
|
| 5 |
|
|
|
|
| 19 |
activeTab: SidebarTab;
|
| 20 |
onTabSelect: Dispatch<SetStateAction<SidebarTab>>;
|
| 21 |
status: string;
|
| 22 |
+
lang: Language;
|
| 23 |
};
|
| 24 |
|
| 25 |
+
export default function Sidebar({ tabs, activeTab, onTabSelect, status, lang }: Props) {
|
| 26 |
+
const t = translations[lang];
|
| 27 |
+
|
| 28 |
+
const getTabLabel = (tab: SidebarTab) => {
|
| 29 |
+
switch(tab) {
|
| 30 |
+
case "Dashboard": return t.dashboard;
|
| 31 |
+
case "Tender Search": return t.tenderSearch;
|
| 32 |
+
case "My Portfolio": return t.myPortfolio;
|
| 33 |
+
case "Company Profile": return t.companyProfile;
|
| 34 |
+
case "Agent Analysis": return t.agentAnalysis;
|
| 35 |
+
case "Proposal Draft": return t.proposalDraft;
|
| 36 |
+
case "Reports": return t.reports;
|
| 37 |
+
case "History": return t.history;
|
| 38 |
+
case "About": return t.about;
|
| 39 |
+
default: return tab;
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
return (
|
| 43 |
<aside className="w-72 glass-card rounded-3xl h-[calc(100vh-3rem)] sticky top-6 p-6 flex flex-col gap-8">
|
| 44 |
<div className="flex items-center gap-3 px-2">
|
|
|
|
| 69 |
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${
|
| 70 |
isActive ? "bg-purple-500 scale-125 shadow-[0_0_8px_rgba(168,85,247,0.8)]" : "bg-transparent group-hover:bg-slate-600"
|
| 71 |
}`} />
|
| 72 |
+
<span className="font-medium text-sm">{getTabLabel(tab)}</span>
|
| 73 |
</button>
|
| 74 |
);
|
| 75 |
})}
|
frontend/components/TenderSearch.tsx
CHANGED
|
@@ -10,9 +10,11 @@ type Props = {
|
|
| 10 |
onAnalyze: (tender: Tender) => void;
|
| 11 |
forceShowFollowed?: boolean;
|
| 12 |
initialKeyword?: string;
|
|
|
|
| 13 |
};
|
| 14 |
|
| 15 |
-
export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "" }: Props) {
|
|
|
|
| 16 |
const [keyword, setKeyword] = useState(initialKeyword);
|
| 17 |
const [buyerCode, setBuyerCode] = useState("");
|
| 18 |
const [date, setDate] = useState("");
|
|
@@ -195,10 +197,10 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 195 |
<table className="w-full text-left text-sm table-fixed border-collapse">
|
| 196 |
<thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold border-b border-white/5">
|
| 197 |
<tr>
|
| 198 |
-
<th className="px-4 py-5 w-[100px]">
|
| 199 |
-
<th className="px-4 py-5 w-[250px]">
|
| 200 |
-
<th className="px-4 py-5 w-[180px]">
|
| 201 |
-
<th className="px-4 py-5 text-center w-[100px]">
|
| 202 |
</tr>
|
| 203 |
</thead>
|
| 204 |
<tbody className="divide-y divide-white/5">
|
|
@@ -365,7 +367,7 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 365 |
}}
|
| 366 |
className="premium-gradient text-white px-8 py-3 rounded-xl font-bold text-sm shadow-xl shadow-purple-500/20 active:scale-95 transition-all"
|
| 367 |
>
|
| 368 |
-
|
| 369 |
</button>
|
| 370 |
</div>
|
| 371 |
</div>
|
|
|
|
| 10 |
onAnalyze: (tender: Tender) => void;
|
| 11 |
forceShowFollowed?: boolean;
|
| 12 |
initialKeyword?: string;
|
| 13 |
+
lang: Language;
|
| 14 |
};
|
| 15 |
|
| 16 |
+
export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "", lang }: Props) {
|
| 17 |
+
const t = translations[lang];
|
| 18 |
const [keyword, setKeyword] = useState(initialKeyword);
|
| 19 |
const [buyerCode, setBuyerCode] = useState("");
|
| 20 |
const [date, setDate] = useState("");
|
|
|
|
| 197 |
<table className="w-full text-left text-sm table-fixed border-collapse">
|
| 198 |
<thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold border-b border-white/5">
|
| 199 |
<tr>
|
| 200 |
+
<th className="px-4 py-5 w-[100px]">{t.idSelect}</th>
|
| 201 |
+
<th className="px-4 py-5 w-[250px]">{t.opportunity}</th>
|
| 202 |
+
<th className="px-4 py-5 w-[180px]">{t.buyer}</th>
|
| 203 |
+
<th className="px-4 py-5 text-center w-[100px]">{t.status}</th>
|
| 204 |
</tr>
|
| 205 |
</thead>
|
| 206 |
<tbody className="divide-y divide-white/5">
|
|
|
|
| 367 |
}}
|
| 368 |
className="premium-gradient text-white px-8 py-3 rounded-xl font-bold text-sm shadow-xl shadow-purple-500/20 active:scale-95 transition-all"
|
| 369 |
>
|
| 370 |
+
{t.analyze}
|
| 371 |
</button>
|
| 372 |
</div>
|
| 373 |
</div>
|
frontend/lib/translations.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const translations = {
|
| 2 |
+
en: {
|
| 3 |
+
dashboard: "Dashboard",
|
| 4 |
+
tenderSearch: "Tender Search",
|
| 5 |
+
myPortfolio: "My Portfolio",
|
| 6 |
+
companyProfile: "Company Profile",
|
| 7 |
+
agentAnalysis: "Agent Analysis",
|
| 8 |
+
proposalDraft: "Proposal Draft",
|
| 9 |
+
reports: "Reports",
|
| 10 |
+
history: "History",
|
| 11 |
+
about: "About",
|
| 12 |
+
resumenEjecutivo: "Executive Summary",
|
| 13 |
+
andesOpsDesc: "Market intelligence and agentic analysis for public tenders.",
|
| 14 |
+
syncPipeline: "Sync Global Pipeline",
|
| 15 |
+
tendersFound: "Tenders Found",
|
| 16 |
+
activeOpps: "Active opportunities",
|
| 17 |
+
recommended: "Recommended",
|
| 18 |
+
highRisk: "High Risk",
|
| 19 |
+
totalPipeline: "Total Pipeline",
|
| 20 |
+
sectors: "Market Sectors",
|
| 21 |
+
regionalDist: "Regional Distribution",
|
| 22 |
+
deadlines: "Deadline Status",
|
| 23 |
+
integrityMonitor: "Data Integrity Monitor",
|
| 24 |
+
recentActivity: "Recent Pipeline Activity",
|
| 25 |
+
idSelect: "ID / Select",
|
| 26 |
+
opportunity: "Opportunity",
|
| 27 |
+
buyer: "Buyer",
|
| 28 |
+
status: "Status",
|
| 29 |
+
analyze: "Analyze",
|
| 30 |
+
esgScore: "ESG Compliance Rating",
|
| 31 |
+
environmental: "Environmental",
|
| 32 |
+
social: "Social",
|
| 33 |
+
governance: "Governance",
|
| 34 |
+
language: "Language",
|
| 35 |
+
ingesting: "INGESTING DOCUMENTS...",
|
| 36 |
+
analyzeSelected: "ANALYZE SELECTED",
|
| 37 |
+
},
|
| 38 |
+
es: {
|
| 39 |
+
dashboard: "Panel de Control",
|
| 40 |
+
tenderSearch: "Buscador de Licitaciones",
|
| 41 |
+
myPortfolio: "Mi Portafolio",
|
| 42 |
+
companyProfile: "Perfil de Empresa",
|
| 43 |
+
agentAnalysis: "An谩lisis Ag茅ntico",
|
| 44 |
+
proposalDraft: "Borrador de Propuesta",
|
| 45 |
+
reports: "Reportes",
|
| 46 |
+
history: "Historial",
|
| 47 |
+
about: "Sistema",
|
| 48 |
+
resumenEjecutivo: "Resumen Ejecutivo",
|
| 49 |
+
andesOpsDesc: "Inteligencia de mercado y an谩lisis de agentes para licitaciones p煤blicas.",
|
| 50 |
+
syncPipeline: "Sincronizar Pipeline Global",
|
| 51 |
+
tendersFound: "Licitaciones Halladas",
|
| 52 |
+
activeOpps: "Oportunidades activas",
|
| 53 |
+
recommended: "Recomendadas",
|
| 54 |
+
highRisk: "Riesgo Alto",
|
| 55 |
+
totalPipeline: "Pipeline Total",
|
| 56 |
+
sectors: "Sectores de Mercado",
|
| 57 |
+
regionalDist: "Distribuci贸n Regional",
|
| 58 |
+
deadlines: "Estado de Plazos",
|
| 59 |
+
integrityMonitor: "Monitor de Integridad de Datos",
|
| 60 |
+
recentActivity: "Actividad Reciente",
|
| 61 |
+
idSelect: "ID / Selecci贸n",
|
| 62 |
+
opportunity: "Oportunidad",
|
| 63 |
+
buyer: "Comprador",
|
| 64 |
+
status: "Estado",
|
| 65 |
+
analyze: "Analizar",
|
| 66 |
+
esgScore: "Calificaci贸n de Cumplimiento ESG",
|
| 67 |
+
environmental: "Ambiental",
|
| 68 |
+
social: "Social",
|
| 69 |
+
governance: "Gobernanza",
|
| 70 |
+
language: "Idioma",
|
| 71 |
+
ingesting: "INGIRIENDO DOCUMENTOS...",
|
| 72 |
+
analyzeSelected: "ANALIZAR SELECCIONADOS",
|
| 73 |
+
}
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
export type Language = "en" | "es";
|