2026-03-02 09:05:18 +01:00
|
|
|
"use client";
|
|
|
|
|
/**
|
2026-03-05 05:18:03 +01:00
|
|
|
* TEE OFF ADMIN DASHBOARD v1.4 - LIVE PROGRESSION
|
2026-03-02 09:05:18 +01:00
|
|
|
* ---------------------------------------------------------------------------
|
|
|
|
|
* PLASSERING: frontend/src/app/admin/page.tsx
|
2026-03-05 05:18:03 +01:00
|
|
|
* FUNKSJON: Starter bakgrunnsjobber og oppdaterer tabellen live.
|
2026-03-02 09:05:18 +01:00
|
|
|
* ---------------------------------------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { API_URL } from "@/config/constants";
|
2026-03-05 05:18:03 +01:00
|
|
|
import ScrapeMethodSelect from "@/components/ScrapeMethodSelect";
|
2026-03-02 09:05:18 +01:00
|
|
|
|
|
|
|
|
export default function AdminDashboard() {
|
2026-03-05 05:18:03 +01:00
|
|
|
const [facilities, setFacilities] = useState<any[]>([]);
|
2026-03-02 09:05:18 +01:00
|
|
|
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-02 09:05:18 +01:00
|
|
|
|
2026-03-05 05:18:03 +01:00
|
|
|
// Henter data fra databasen
|
|
|
|
|
const fetchFacilities = () => {
|
2026-03-02 09:05:18 +01:00
|
|
|
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-02 09:05:18 +01:00
|
|
|
}, []);
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 09:05:18 +01:00
|
|
|
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 på {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>
|
2026-03-02 09:05:18 +01:00
|
|
|
</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>
|
2026-03-02 09:05:18 +01:00
|
|
|
<th className="pb-6">Konfigurasjon</th>
|
2026-03-05 05:18:03 +01:00
|
|
|
<th className="pb-6">Metode</th>
|
2026-03-02 09:05:18 +01:00
|
|
|
<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>
|
2026-03-02 09:05:18 +01:00
|
|
|
</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">
|
2026-03-02 09:05:18 +01:00
|
|
|
<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>
|
2026-03-02 09:05:18 +01:00
|
|
|
<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>
|
2026-03-02 09:05:18 +01:00
|
|
|
<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>
|
2026-03-02 09:05:18 +01:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|