File size: 8,527 Bytes
249ca00 5e52bd7 249ca00 5e52bd7 b294142 5e52bd7 b294142 5e52bd7 b294142 5e52bd7 b294142 5e52bd7 b294142 5e52bd7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import { useState, useEffect } from "react";
import BrandLoader from "./BrandLoader";
import { Language, translations } from "../lib/translations";
import { clearDatabase, fetchDetailedDbStats, syncDatabase } from "../lib/api";
type Props = {
onFilterClick?: (type: "sector" | "region" | "buyer", value: string) => void;
lang: Language;
};
export default function DBManager({ onFilterClick, lang }: Props) {
const t = translations[lang];
const [stats, setStats] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isActionInProgress, setIsActionInProgress] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const loadStats = async () => {
setIsLoading(true);
const data = await fetchDetailedDbStats();
setStats(data);
setIsLoading(false);
};
useEffect(() => {
loadStats();
}, []);
const handleSync = async () => {
setIsActionInProgress(true);
setMessage(null);
try {
const result = await syncDatabase();
setMessage({
type: 'success',
text: `Sync complete! New: ${result.tenders?.new || 0} tenders, ${result.purchase_orders?.new || 0} OCs.`
});
await loadStats();
} catch (e) {
setMessage({ type: 'error', text: 'Synchronization failed.' });
} finally {
setIsActionInProgress(false);
}
};
const handleClear = async () => {
if (!confirm("Are you sure you want to delete ALL local tenders and purchase orders? This cannot be undone.")) return;
setIsActionInProgress(true);
setMessage(null);
try {
await clearDatabase();
setMessage({ type: 'success', text: 'Local database cleared successfully.' });
await loadStats();
} catch (e) {
setMessage({ type: 'error', text: 'Failed to clear database.' });
} finally {
setIsActionInProgress(false);
}
};
if (isLoading) return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-cyan"></div>
</div>
);
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
{isActionInProgress && <BrandLoader />}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h2 className="text-3xl font-black text-white tracking-tight">{t.databaseTitle}</h2>
<p className="text-slate-400 mt-1">{t.databaseDesc}</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleSync}
disabled={isActionInProgress}
className="px-6 py-3 rounded-2xl bg-cyan text-slate-950 font-bold hover:bg-sky transition-all active:scale-95 disabled:opacity-50 flex items-center gap-2"
>
<span>🔄 {lang === 'es' ? 'Sincronizar Todo' : 'Sync Everything'}</span>
</button>
<button
onClick={handleClear}
disabled={isActionInProgress}
className="px-6 py-3 rounded-2xl bg-red-500/10 border border-red-500/30 text-red-400 font-bold hover:bg-red-500/20 transition-all active:scale-95 disabled:opacity-50 flex items-center gap-2"
>
<span>🗑️ {t.cleanDatabase}</span>
</button>
</div>
</div>
{message && (
<div className={`p-4 rounded-2xl border ${message.type === 'success' ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-red-500/10 border-red-500/30 text-red-400'} animate-in zoom-in-95 duration-300`}>
{message.text}
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="glass-card p-8 border border-white/5 bg-white/[0.02] rounded-3xl relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4 text-4xl opacity-10 group-hover:scale-110 transition-transform">📄</div>
<p className="text-xs font-black uppercase tracking-[0.2em] text-slate-500 mb-2">{t.tenderCount}</p>
<h3 className="text-5xl font-black text-white">{stats?.total_records || 0}</h3>
<p className="text-[10px] text-cyan mt-4 font-mono">{lang === 'es' ? 'Sincronización:' : 'Last Sync:'} {stats?.last_sync ? new Date(stats.last_sync).toLocaleString() : (lang === 'es' ? 'Nunca' : 'Never')}</p>
</div>
<div className="glass-card p-8 border border-white/5 bg-white/[0.02] rounded-3xl relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4 text-4xl opacity-10 group-hover:scale-110 transition-transform">🛒</div>
<p className="text-xs font-black uppercase tracking-[0.2em] text-slate-500 mb-2">{lang === 'es' ? 'Órdenes de Compra' : 'Purchase Orders'}</p>
<h3 className="text-5xl font-black text-white">{stats?.total_ocs || 0}</h3>
<p className="text-[10px] text-sky mt-4 font-mono">{lang === 'es' ? 'Seguimiento en tiempo real' : 'Real-time local tracking'}</p>
</div>
<div className="glass-card p-8 border border-white/5 bg-white/[0.02] rounded-3xl relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4 text-4xl opacity-10 group-hover:scale-110 transition-transform">🧠</div>
<p className="text-xs font-black uppercase tracking-[0.2em] text-slate-500 mb-2">{t.analysisCount}</p>
<h3 className="text-5xl font-black text-white">{stats?.total_analysis || 0}</h3>
<p className="text-[10px] text-purple-400 mt-4 font-mono">{lang === 'es' ? 'Inteligencia de IA persistente' : 'AI Intelligence persistence'}</p>
</div>
</div>
{/* Top Buyers List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="glass-card p-8 border border-white/5 bg-white/[0.02] rounded-3xl">
<h3 className="text-sm font-black uppercase tracking-widest text-slate-400 mb-6 flex items-center gap-2">
<span>🏛️</span> {lang === 'es' ? 'Instituciones Top Locales' : 'Top Local Institutions'}
</h3>
<div className="space-y-4">
{stats?.top_buyers?.map((buyer: any, idx: number) => (
<button
key={idx}
onClick={() => onFilterClick?.("buyer", buyer.name)}
className="w-full flex items-center justify-between p-4 rounded-2xl bg-white/[0.03] border border-white/5 hover:bg-white/[0.08] hover:border-cyan/30 transition-all group/row cursor-pointer text-left"
>
<span className="text-sm text-slate-300 truncate max-w-[250px] font-medium group-hover/row:text-white transition-colors">
{buyer.name}
</span>
<div className="flex items-center gap-3">
<span className="text-lg font-black text-cyan font-mono">{buyer.count}</span>
<span className="opacity-0 group-hover/row:opacity-100 transition-opacity text-cyan">📡</span>
</div>
</button>
))}
{(!stats?.top_buyers || stats.top_buyers.length === 0) && (
<p className="text-slate-600 italic text-sm py-4">No institutions found in local database.</p>
)}
</div>
</div>
<div className="glass-card p-8 border border-white/5 bg-white/[0.02] rounded-3xl">
<h3 className="text-sm font-black uppercase tracking-widest text-slate-400 mb-6 flex items-center gap-2">
<span>💡</span> Persistence Insights
</h3>
<div className="space-y-6">
<div className="p-4 rounded-2xl bg-blue-500/5 border border-blue-500/10">
<p className="text-xs text-blue-400 font-bold mb-1">Local Mode Active</p>
<p className="text-xs text-slate-400 leading-relaxed">System is prioritizing local database for faster search. Global sync updates the local cache with the latest Mercado Público data.</p>
</div>
<div className="p-4 rounded-2xl bg-purple-500/5 border border-purple-500/10">
<p className="text-xs text-purple-400 font-bold mb-1">Integrity Check</p>
<p className="text-xs text-slate-400 leading-relaxed">All nested data (attachments, items, criteria) is successfully serialized as JSON in the SQLite storage.</p>
</div>
</div>
</div>
</div>
</div>
);
}
|