Nye-TeeOff/frontend/src/app/admin/page.tsx

206 lines
9.3 KiB
TypeScript
Raw Normal View History

"use client";
/**
2026-03-05 05:18:03 +01:00
* TEE OFF ADMIN DASHBOARD v1.4 - LIVE PROGRESSION
* ---------------------------------------------------------------------------
* PLASSERING: frontend/src/app/admin/page.tsx
2026-03-05 05:18:03 +01:00
* FUNKSJON: Starter bakgrunnsjobber og oppdaterer tabellen live.
* ---------------------------------------------------------------------------
*/
import { useState, useEffect } from 'react';
import { API_URL } from "@/config/constants";
2026-03-05 05:18:03 +01:00
import ScrapeMethodSelect from "@/components/ScrapeMethodSelect";
export default function AdminDashboard() {
2026-03-05 05:18:03 +01:00
const [facilities, setFacilities] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
2026-03-05 05:18:03 +01:00
const [selectedFacilities, setSelectedFacilities] = useState<number[]>([]);
// NYTT: Holder styr på om en skraping pågår akkurat nå
const [isScraping, setIsScraping] = useState(false);
2026-03-05 05:18:03 +01:00
// Henter data fra databasen
const fetchFacilities = () => {
fetch(`${API_URL}/facilities`)
.then(res => res.json())
.then(data => {
setFacilities(Array.isArray(data) ? data : []);
setLoading(false);
})
.catch(() => setLoading(false));
2026-03-05 05:18:03 +01:00
};
// Hent data ved første innlasting
useEffect(() => {
fetchFacilities();
}, []);
2026-03-05 05:18:03 +01:00
// NYTT: Hvis skraping pågår, oppdater tabellen hvert 10. sekund!
useEffect(() => {
let interval: NodeJS.Timeout;
if (isScraping) {
interval = setInterval(() => {
fetchFacilities();
}, 10000);
}
return () => clearInterval(interval);
}, [isScraping]);
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelectedFacilities(facilities.map(f => f.id));
} else {
setSelectedFacilities([]);
}
};
const handleSelectOne = (id: number, checked: boolean) => {
if (checked) {
setSelectedFacilities([...selectedFacilities, id]);
} else {
setSelectedFacilities(selectedFacilities.filter(facilityId => facilityId !== id));
}
};
// NYTT: Sender IDene til API-et og starter auto-oppdatering
const handleRunScrapers = async () => {
setIsScraping(true);
try {
const response = await fetch(`${API_URL}/admin/run-scraper`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ facility_ids: selectedFacilities })
});
if (!response.ok) throw new Error("Kunne ikke starte skraping");
// Valgfritt: Fjern avhukingene når jobben har startet
setSelectedFacilities([]);
// Stopper auto-oppdateringen etter 10 minutter (for sikkerhets skyld)
setTimeout(() => setIsScraping(false), 10 * 60 * 1000);
} catch (error) {
console.error(error);
alert("Feil ved start av skraperen.");
setIsScraping(false);
}
};
if (loading) return <div className="p-20 text-center font-black animate-pulse">LASTER DASHBORD...</div>;
return (
<div className="flex flex-col lg:flex-row min-h-screen bg-[#f1f7ed] font-sans">
<aside className="lg:w-[22%] bg-[#11280f] text-white p-10 flex flex-col">
<h1 className="text-2xl font-black uppercase tracking-tighter mb-10">TeeOff Admin</h1>
<nav className="space-y-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#7ca982] flex-grow">
<div className="text-white border-l-4 border-[#8bc34a] pl-4 py-1">Scraping Monitor</div>
<div className="hover:text-white cursor-pointer pl-4 py-1 transition-colors">Medlemskap</div>
<div className="hover:text-white cursor-pointer pl-4 py-1 transition-colors">Bildegalleri</div>
</nav>
<div className="mt-auto pt-10 border-t border-white/10">
<button onClick={() => window.location.href='/'} className="text-[10px] font-black uppercase tracking-widest text-red-400 hover:text-red-300">Logg ut</button>
</div>
</aside>
<main className="lg:w-[78%] p-6 md:p-12">
<div className="bg-white rounded-[3rem] shadow-2xl p-10 md:p-16 border border-white">
<header className="flex justify-between items-center mb-12">
<div>
<h2 className="text-4xl font-black tracking-tighter text-[#11280f] mb-2">Scraping Monitor</h2>
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest">Sjekker status {facilities.length} anlegg</p>
</div>
2026-03-05 05:18:03 +01:00
{/* NYTT: Knappen endrer utseende når skraping pågår */}
<button
onClick={handleRunScrapers}
disabled={selectedFacilities.length === 0 || isScraping}
className={`text-white px-8 py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest shadow-xl transition-all
${isScraping
? 'bg-yellow-500 animate-pulse cursor-wait'
: 'bg-[#8bc34a] hover:scale-105 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed'
}`}
>
{isScraping ? '🤖 Skraper... Henter live data' : `Kjør valgte skrapere (${selectedFacilities.length})`}
</button>
</header>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="text-[10px] font-black uppercase tracking-widest text-gray-300 border-b border-gray-50">
2026-03-05 05:18:03 +01:00
<th className="pb-6 pl-6 w-10">
<input
type="checkbox"
className="w-4 h-4 cursor-pointer accent-[#8bc34a]"
checked={selectedFacilities.length === facilities.length && facilities.length > 0}
onChange={handleSelectAll}
/>
</th>
<th className="pb-6 pr-10">Anlegg</th>
<th className="pb-6">Konfigurasjon</th>
2026-03-05 05:18:03 +01:00
<th className="pb-6">Metode</th>
<th className="pb-6">Siste Sjekk</th>
2026-03-05 05:18:03 +01:00
<th className="pb-6">Banestatus</th>
<th className="pb-6 text-right">Handling</th>
</tr>
</thead>
<tbody className="text-sm font-bold text-[#11280f]">
{facilities.map((f: any) => (
<tr key={f.id} className="border-b border-gray-50 group hover:bg-gray-50/50 transition-colors">
2026-03-05 05:18:03 +01:00
<td className="py-8 pl-6 w-10">
<input
type="checkbox"
className="w-4 h-4 cursor-pointer accent-[#8bc34a]"
checked={selectedFacilities.includes(f.id)}
onChange={(e) => handleSelectOne(f.id, e.target.checked)}
/>
</td>
<td className="py-8 pl-10">
<div className="font-black text-lg">{f.name}</div>
<div className="text-[10px] text-[#7ca982] uppercase tracking-widest">{f.city}</div>
</td>
<td className="py-8">
2026-03-05 05:18:03 +01:00
<div className="text-[11px] text-blue-600 truncate max-w-[200px] mb-1">{f.scrape_status_url}</div>
<div className="text-[10px] font-mono text-gray-300">{f.scrape_status_selector}</div>
</td>
2026-03-05 05:18:03 +01:00
<td className="py-8 pr-4">
<ScrapeMethodSelect facility={f} />
</td>
<td className="py-8 text-gray-400 font-mono text-xs">
{f.status_updated_at ? new Date(f.status_updated_at).toLocaleDateString('nb-NO') : 'Aldri'}
</td>
2026-03-05 05:18:03 +01:00
<td className="py-8">
<div className="flex flex-col gap-2">
{f.course_statuses && f.course_statuses.map((cs: any, idx: number) => {
let badgeColor = "bg-gray-100 text-gray-500";
if (cs.status === "aapen") badgeColor = "bg-green-100 text-green-700";
if (cs.status === "stengt" || cs.status === "nedlagt") badgeColor = "bg-red-100 text-red-700";
if (cs.status === "aapen_med_vintergreener" || cs.status === "aapner_snart") badgeColor = "bg-yellow-100 text-yellow-700";
return (
<div key={idx} className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-widest text-gray-400 truncate max-w-[100px]" title={cs.name}>
{cs.name}
</span>
<span className={`px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest ${badgeColor}`}>
{cs.status || 'UKJENT'}
</span>
</div>
)
})}
</div>
</td>
<td className="py-8 text-right">
<button className="bg-gray-100 px-5 py-2.5 rounded-xl text-[9px] font-black uppercase tracking-widest hover:bg-[#11280f] hover:text-white transition-all">Rediger</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</main>
</div>
);
}