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

456 lines
27 KiB
TypeScript
Raw Normal View History

"use client";
/**
2026-03-12 13:39:10 +01:00
* TEE OFF ADMIN DASHBOARD v2.1 - MISSION CONTROL + INLINE EDIT
* ---------------------------------------------------------------------------
2026-03-12 13:39:10 +01:00
* FUNKSJON: "Mission Control" med faner for Banestatus og Medlemskap,
* og klikk-for-å-redigere URL-er direkte i tabellen!
* ---------------------------------------------------------------------------
*/
2026-03-06 09:39:32 +01:00
import { useState, useEffect, useMemo } from 'react';
import { API_URL } from "@/config/constants";
2026-03-05 09:25:15 +01:00
import ScrapeMethodSelect from "@/components/ScrapeMethodSelect";
2026-03-07 09:46:33 +01:00
import Link from 'next/link';
2026-03-12 13:39:10 +01:00
// KOMPONENT FOR HURTIGREDIGERING AV URL-ER
const InlineEdit = ({ facilityId, field, initialValue, onSave }: { facilityId: number, field: string, initialValue: string, onSave: (id: number, field: string, val: string) => void }) => {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue || '');
const handleSave = () => {
setIsEditing(false);
if (value !== initialValue) {
onSave(facilityId, field, value);
}
};
if (isEditing) {
return (
<div className="flex flex-col gap-1 w-full max-w-[200px] animate-fade-in">
<textarea
autoFocus
rows={2}
className="border-2 border-[#8bc34a] p-2 text-[10px] w-full rounded-lg outline-none resize-y shadow-sm font-mono text-black bg-white"
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSave(); } }}
placeholder="Lim inn URL(er)..."
/>
<div className="flex gap-1">
<button onClick={handleSave} className="bg-[#8bc34a] text-white px-3 py-1.5 rounded-md text-[10px] font-black uppercase flex-1 shadow-sm hover:bg-[#7ca982]">Lagre</button>
<button onClick={() => { setIsEditing(false); setValue(initialValue || ''); }} className="bg-gray-200 text-gray-600 px-3 py-1.5 rounded-md text-[10px] font-black uppercase hover:bg-gray-300">Avbryt</button>
</div>
</div>
);
}
return (
<div className="group flex items-start gap-2 cursor-pointer p-1.5 -ml-1.5 rounded-lg hover:bg-white border border-transparent hover:border-gray-200 hover:shadow-sm transition-all" onClick={() => setIsEditing(true)} title="Klikk for å redigere URL">
<div className="text-[10px] text-blue-600 break-all max-w-[150px] leading-tight line-clamp-2">
{initialValue ? initialValue : <span className="text-red-400 italic">Mangler URL</span>}
</div>
<span className="opacity-0 group-hover:opacity-100 text-[10px] bg-gray-100 p-1 rounded transition-opacity"></span>
</div>
);
};
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[]>([]);
const [isScraping, setIsScraping] = useState(false);
2026-03-05 09:25:15 +01:00
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [editingFacility, setEditingFacility] = useState<any | null>(null);
2026-03-12 13:39:10 +01:00
const [activeTab, setActiveTab] = useState<'banestatus' | 'medlemskap'>('banestatus');
2026-03-06 09:39:32 +01:00
const [statusFilter, setStatusFilter] = useState('alle');
2026-03-12 13:39:10 +01:00
const [editForm, setEditForm] = useState({ scrape_status_url: '', scrape_status_selector: '', scrape_method: '', ai_instruction: '', courses: [] as any[] });
2026-03-05 09:25:15 +01:00
const [isSaving, setIsSaving] = useState(false);
2026-03-05 05:18:03 +01:00
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
};
useEffect(() => {
fetchFacilities();
}, []);
2026-03-05 05:18:03 +01:00
useEffect(() => {
let interval: NodeJS.Timeout;
if (isScraping) {
2026-03-12 13:39:10 +01:00
interval = setInterval(() => fetchFacilities(), 10000);
2026-03-05 05:18:03 +01:00
}
return () => clearInterval(interval);
}, [isScraping]);
2026-03-12 13:39:10 +01:00
useEffect(() => {
setSelectedFacilities([]);
}, [activeTab]);
2026-03-06 09:39:32 +01:00
const filteredFacilities = useMemo(() => {
if (statusFilter === 'alle') return facilities;
return facilities.map(facility => {
if (!facility.course_statuses) return facility;
const filteredCourses = facility.course_statuses.filter((cs: any) => {
const s = cs.status || 'ukjent';
2026-03-12 13:39:10 +01:00
if (statusFilter === 'aapne') return s === 'aapen';
if (statusFilter === 'ikke_stengt') return ['aapen', 'aapen_med_vintergreener', 'aapner_snart'].includes(s);
if (statusFilter === 'stengt') return s === 'stengt' || s === 'nedlagt';
if (statusFilter === 'ukjent_feil') return s === 'ukjent' || s === 'NOT_FOUND';
2026-03-06 09:39:32 +01:00
return true;
});
return { ...facility, course_statuses: filteredCourses };
}).filter(facility => facility.course_statuses && facility.course_statuses.length > 0);
}, [facilities, statusFilter]);
2026-03-05 05:18:03 +01:00
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
2026-03-12 13:39:10 +01:00
if (e.target.checked) setSelectedFacilities(filteredFacilities.map(f => f.id));
else setSelectedFacilities([]);
2026-03-05 05:18:03 +01:00
};
const handleSelectOne = (id: number, checked: boolean) => {
2026-03-12 13:39:10 +01:00
if (checked) setSelectedFacilities([...selectedFacilities, id]);
else setSelectedFacilities(selectedFacilities.filter(facilityId => facilityId !== id));
2026-03-05 05:18:03 +01:00
};
2026-03-12 13:39:10 +01:00
// Funksjon for å lagre inline redigering av URLer
const handleQuickEdit = async (id: number, field: string, value: string) => {
// "Optimistisk oppdatering" - Vi bytter teksten i skjermen med én gang så det føles lynraskt!
setFacilities(facilities.map(f => f.id === id ? { ...f, [field]: value } : f));
try {
const res = await fetch(`${API_URL}/admin/facilities/${id}/quick-edit`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ field, value })
});
if (!res.ok) throw new Error("Feil ved lagring");
} catch (e) {
alert("Kunne ikke lagre endringen i databasen.");
fetchFacilities(); // Henter gamle data tilbake hvis det kræsjet
2026-03-05 09:25:15 +01:00
}
2026-03-12 13:39:10 +01:00
};
2026-03-05 09:25:15 +01:00
2026-03-12 13:39:10 +01:00
const handleRunScrapers = async () => {
if (isScraping) { setIsScraping(false); return; }
2026-03-05 05:18:03 +01:00
setIsScraping(true);
2026-03-12 13:39:10 +01:00
const endpoint = activeTab === 'banestatus' ? '/admin/run-scraper' : '/admin/run-membership-scraper';
2026-03-05 05:18:03 +01:00
try {
2026-03-12 13:39:10 +01:00
const response = await fetch(`${API_URL}${endpoint}`, {
2026-03-05 05:18:03 +01:00
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ facility_ids: selectedFacilities })
});
if (!response.ok) throw new Error("Kunne ikke starte skraping");
2026-03-05 09:25:15 +01:00
const timeoutMs = Math.max(selectedFacilities.length * 40 * 1000, 60000);
setSelectedFacilities([]);
setTimeout(() => setIsScraping(false), timeoutMs);
2026-03-05 05:18:03 +01:00
} catch (error) {
2026-03-12 13:39:10 +01:00
alert(`Feil ved start av ${activeTab}-skraperen.`);
2026-03-05 05:18:03 +01:00
setIsScraping(false);
}
};
2026-03-05 09:25:15 +01:00
const openEditModal = (facility: any) => {
setEditingFacility(facility);
setEditForm({
scrape_status_url: facility.scrape_status_url || '',
scrape_status_selector: facility.scrape_status_selector || '',
scrape_method: facility.scrape_method || 'css_selector',
ai_instruction: facility.ai_instruction || '',
courses: facility.course_statuses ? facility.course_statuses.map((c: any) => ({id: c.id, name: c.name, status: c.status})) : []
});
};
const handleSaveEdit = async () => {
setIsSaving(true);
try {
const response = await fetch(`${API_URL}/admin/facilities/${editingFacility.id}/scrape-settings`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editForm)
});
if (!response.ok) throw new Error("Feil ved lagring");
setEditingFacility(null);
fetchFacilities();
} catch (error) {
alert("Kunne ikke lagre endringene.");
} finally {
setIsSaving(false);
}
};
if (loading) return <div className="p-20 text-center font-black animate-pulse">LASTER DASHBORD...</div>;
return (
2026-03-05 09:25:15 +01:00
<div className="flex min-h-screen bg-[#f1f7ed] font-sans relative overflow-hidden">
2026-03-12 13:39:10 +01:00
{/* REDIGER-MODAL FOR BANESTATUS-SKRAPING */}
2026-03-05 09:25:15 +01:00
{editingFacility && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-lg overflow-hidden flex flex-col max-h-[90vh]">
<div className="bg-[#11280f] text-white p-6 shrink-0">
<h3 className="text-xl font-black uppercase tracking-widest">Skrape-innstillinger</h3>
2026-03-05 09:25:15 +01:00
<p className="text-sm text-[#7ca982]">{editingFacility.name}</p>
</div>
<div className="p-8 space-y-6 overflow-y-auto flex-grow">
<div>
2026-03-12 13:39:10 +01:00
<label className="block text-xs font-bold text-gray-500 uppercase tracking-widest mb-2">Scrape URL (Banestatus)</label>
<input type="text" value={editForm.scrape_status_url} onChange={(e) => setEditForm({...editForm, scrape_status_url: e.target.value})} className="w-full border-2 border-gray-100 rounded-xl p-3 text-sm focus:border-[#8bc34a] focus:outline-none transition-colors" placeholder="f.eks. https://golfklubb.no/banestatus" />
2026-03-05 09:25:15 +01:00
</div>
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-widest mb-2">Skrapemetode</label>
2026-03-12 13:39:10 +01:00
<select value={editForm.scrape_method} onChange={(e) => setEditForm({...editForm, scrape_method: e.target.value})} className="w-full border-2 border-gray-100 rounded-xl p-3 text-sm focus:border-[#8bc34a] focus:outline-none transition-colors">
2026-03-05 09:25:15 +01:00
<option value="css_selector">Standard (CSS)</option>
<option value="llm_parse"> Gemini AI (LLM)</option>
<option value="iframe_golfbox">Golfbox iframe</option>
<option value="click_then_css">Auto-klikk + CSS</option>
<option value="manual">🚨 Manuell (Ikke skrap)</option>
</select>
2026-03-06 09:39:32 +01:00
</div>
2026-03-05 09:25:15 +01:00
{editForm.scrape_method === 'llm_parse' && (
<div className="animate-fade-in">
<label className="block text-xs font-bold text-[#8bc34a] uppercase tracking-widest mb-2"> AI-Hviskeren (Instruks til Gemini)</label>
2026-03-12 13:39:10 +01:00
<textarea value={editForm.ai_instruction || ''} onChange={(e) => setEditForm({...editForm, ai_instruction: e.target.value})} className="w-full border-2 border-[#8bc34a]/30 rounded-xl p-3 text-sm focus:border-[#8bc34a] focus:outline-none transition-colors" placeholder="F.eks: Ignorer info om korthullsbanen. Banen er åpen." rows={3} />
2026-03-05 09:25:15 +01:00
</div>
)}
{editForm.scrape_method === 'manual' && (
<div className="bg-red-50 border border-red-100 rounded-xl p-4 animate-fade-in">
<label className="block text-xs font-black text-red-500 uppercase tracking-widest mb-4">🚨 Sett Status Manuelt</label>
<div className="space-y-4">
{editForm.courses.map((course: any, idx: number) => (
<div key={course.id} className="flex justify-between items-center bg-white p-3 rounded-lg shadow-sm">
<span className="text-xs font-bold text-gray-700 uppercase tracking-widest truncate mr-2" title={course.name}>{course.name}</span>
2026-03-12 13:39:10 +01:00
<select value={course.status || 'ukjent'} onChange={(e) => { const newCourses = [...editForm.courses]; newCourses[idx].status = e.target.value; setEditForm({...editForm, courses: newCourses}); }} className="border border-gray-200 rounded-lg p-2 text-xs font-bold focus:outline-none focus:border-red-400 shrink-0">
2026-03-05 09:25:15 +01:00
<option value="aapen">🟢 Åpen</option>
<option value="aapen_med_vintergreener">🟡 Vintergreener</option>
<option value="aapner_snart">🟡 Åpner Snart</option>
<option value="stengt">🔴 Stengt</option>
<option value="stenger_snart">🔴 Stenger Snart</option>
<option value="under_utvikling">🔨 Under Utvikling</option>
<option value="nedlagt"> Nedlagt</option>
<option value="ukjent"> Ukjent</option>
</select>
</div>
))}
</div>
</div>
2026-03-06 09:39:32 +01:00
)}
2026-03-05 09:25:15 +01:00
{(editForm.scrape_method === 'css_selector' || editForm.scrape_method === 'click_then_css' || editForm.scrape_method === 'iframe_golfbox') && (
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-widest mb-2">CSS Selector</label>
2026-03-12 13:39:10 +01:00
<input type="text" value={editForm.scrape_status_selector} onChange={(e) => setEditForm({...editForm, scrape_status_selector: e.target.value})} className="w-full border-2 border-gray-100 rounded-xl p-3 text-sm focus:border-[#8bc34a] focus:outline-none transition-colors font-mono" placeholder="f.eks. .status-text" />
2026-03-05 09:25:15 +01:00
</div>
)}
</div>
<div className="bg-gray-50 p-6 flex justify-end gap-4 shrink-0">
<button onClick={() => setEditingFacility(null)} className="px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-widest text-gray-500 hover:bg-gray-200 transition-colors">Avbryt</button>
<button onClick={handleSaveEdit} disabled={isSaving} className="bg-[#8bc34a] text-white px-6 py-3 rounded-xl text-xs font-black uppercase tracking-widest shadow-lg hover:scale-105 transition-all disabled:opacity-50">
{isSaving ? 'Lagrer...' : 'Lagre endringer'}
</button>
</div>
</div>
</div>
)}
2026-03-06 09:39:32 +01:00
{/* SIDEBAR */}
2026-03-05 09:25:15 +01:00
<aside className={`bg-[#11280f] text-white flex flex-col transition-all duration-300 shrink-0 ${isSidebarCollapsed ? 'w-16 p-4' : 'w-64 p-8'} hidden md:flex`}>
<div className={`flex items-center mb-10 ${isSidebarCollapsed ? 'justify-center' : 'justify-between'}`}>
{!isSidebarCollapsed && <h1 className="text-2xl font-black uppercase tracking-tighter">TeeOff</h1>}
2026-03-12 13:39:10 +01:00
<button onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} className="text-2xl hover:text-[#8bc34a] transition-colors" title="Skjul/Vis meny"></button>
2026-03-05 09:25:15 +01:00
</div>
<nav className="space-y-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#7ca982] flex-grow">
2026-03-12 13:39:10 +01:00
<Link href="/admin" className={`block hover:text-white cursor-pointer py-1 transition-colors ${isSidebarCollapsed ? 'pl-0 text-center text-xs' : 'pl-4 border-l-4 border-[#8bc34a] text-white'}`} title="Mission Control">
{isSidebarCollapsed ? 'MC' : 'Mission Control'}
</Link>
2026-03-12 13:39:10 +01:00
<Link href="/admin/medlemskap" className={`block hover:text-white cursor-pointer py-1 transition-colors ${isSidebarCollapsed ? 'pl-0 text-center text-xs' : 'pl-4 border-l-4 border-transparent'}`} title="Medlemskaps-vaskeriet">
{isSidebarCollapsed ? 'V' : 'Vaskeriet'}
</Link>
2026-03-05 09:25:15 +01:00
<div className={`hover:text-white cursor-pointer py-1 transition-colors ${isSidebarCollapsed ? 'pl-0 text-center text-xs' : 'pl-4 border-l-4 border-transparent'}`} title="Bildegalleri">
{isSidebarCollapsed ? 'B' : 'Bildegalleri'}
</div>
</nav>
2026-03-05 09:25:15 +01:00
<div className={`mt-auto pt-8 border-t border-white/10 ${isSidebarCollapsed ? 'text-center' : ''}`}>
<button onClick={() => window.location.href='/'} className={`text-[10px] font-black uppercase tracking-widest text-red-400 hover:text-red-300 ${isSidebarCollapsed ? 'writing-vertical' : ''}`} title="Logg ut">
{isSidebarCollapsed ? 'UT' : 'Logg ut'}
</button>
</div>
</aside>
2026-03-06 09:39:32 +01:00
{/* HOVEDINNHOLD */}
2026-03-05 09:25:15 +01:00
<main className="flex-1 min-w-0 p-4 md:p-8 lg:p-10 h-screen overflow-y-auto">
<div className="md:hidden flex justify-between items-center mb-6 bg-[#11280f] text-white p-4 rounded-2xl">
<h1 className="text-xl font-black uppercase tracking-tighter">TeeOff Admin</h1>
2026-03-12 13:39:10 +01:00
<span className="text-xs font-bold text-[#8bc34a]">CONTROL</span>
2026-03-05 09:25:15 +01:00
</div>
<div className="bg-white rounded-[2rem] shadow-2xl p-6 lg:p-10 border border-white">
2026-03-12 13:39:10 +01:00
<header className="flex flex-col xl:flex-row justify-between items-start xl:items-center gap-6 mb-8">
<div>
2026-03-12 13:39:10 +01:00
<h2 className="text-3xl md:text-4xl font-black tracking-tighter text-[#11280f] mb-2">Mission Control</h2>
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest">Sjekker status {filteredFacilities.length} anlegg</p>
</div>
2026-03-12 13:39:10 +01:00
2026-03-05 05:18:03 +01:00
<button
onClick={handleRunScrapers}
2026-03-05 09:25:15 +01:00
disabled={selectedFacilities.length === 0 && !isScraping}
className={`text-white px-6 py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest shadow-xl transition-all whitespace-nowrap
2026-03-05 05:18:03 +01:00
${isScraping
2026-03-05 09:25:15 +01:00
? 'bg-yellow-500 animate-pulse cursor-pointer hover:bg-yellow-600'
2026-03-05 05:18:03 +01:00
: 'bg-[#8bc34a] hover:scale-105 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed'
}`}
>
2026-03-12 13:39:10 +01:00
{isScraping ? '🤖 Skraper... Klikk for å avslutte' : `Kjør ${activeTab}-skrapere (${selectedFacilities.length})`}
2026-03-05 05:18:03 +01:00
</button>
</header>
2026-03-06 09:39:32 +01:00
2026-03-12 13:39:10 +01:00
<div className="flex gap-2 mb-8 border-b-2 border-gray-100 pb-0">
<button onClick={() => setActiveTab('banestatus')} className={`px-6 py-3 text-xs font-black uppercase tracking-widest rounded-t-xl transition-all ${activeTab === 'banestatus' ? 'bg-[#8bc34a] text-white' : 'bg-gray-50 text-gray-400 hover:bg-gray-100'}`}>Banestatus</button>
<button onClick={() => setActiveTab('medlemskap')} className={`px-6 py-3 text-xs font-black uppercase tracking-widest rounded-t-xl transition-all ${activeTab === 'medlemskap' ? 'bg-[#8bc34a] text-white' : 'bg-gray-50 text-gray-400 hover:bg-gray-100'}`}>Medlemskap</button>
2026-03-06 09:39:32 +01:00
</div>
2026-03-12 13:39:10 +01:00
{activeTab === 'banestatus' && (
<div className="flex flex-wrap items-center gap-4 bg-gray-50 p-4 rounded-2xl border border-gray-100 mb-8 animate-fade-in">
<label htmlFor="statusFilter" className="text-xs font-bold text-gray-500 uppercase tracking-widest">Filtrer status:</label>
<select id="statusFilter" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="border-2 border-gray-200 rounded-xl p-2 text-sm font-bold text-[#11280f] focus:border-[#8bc34a] focus:outline-none transition-colors cursor-pointer">
<option value="alle">Vis alle anlegg</option>
<option value="aapne">🟢 Kun åpne baner</option>
<option value="ikke_stengt">🟡 Ikke stengt (Åpne/Vintergreen/Snart)</option>
<option value="stengt">🔴 Kun stengte baner</option>
<option value="ukjent_feil"> Ukjent / Skrapefeil</option>
</select>
</div>
)}
2026-03-05 09:25:15 +01:00
<div className="overflow-x-auto pb-4">
<table className="w-full text-left border-collapse min-w-[800px]">
2026-03-06 09:39:32 +01:00
<thead>
<tr className="text-[10px] font-black uppercase tracking-widest text-gray-300 border-b border-gray-50">
2026-03-05 09:25:15 +01:00
<th className="pb-4 pl-4 w-10">
2026-03-12 13:39:10 +01:00
<input type="checkbox" className="w-4 h-4 cursor-pointer accent-[#8bc34a]" checked={selectedFacilities.length === filteredFacilities.length && filteredFacilities.length > 0} onChange={handleSelectAll} />
2026-03-05 05:18:03 +01:00
</th>
<th className="pb-4 w-12 text-center">ID</th>
2026-03-05 09:25:15 +01:00
<th className="pb-4 pr-6">Anlegg</th>
2026-03-12 13:39:10 +01:00
{activeTab === 'banestatus' ? (
<>
<th className="pb-4">Konfigurasjon (URL & Selektor)</th>
<th className="pb-4">Metode</th>
<th className="pb-4">Siste Sjekk</th>
<th className="pb-4">Banestatus</th>
</>
) : (
<>
<th className="pb-4">Prisside (Klikk for å redigere URL)</th>
<th className="pb-4">Nåværende Priser</th>
<th className="pb-4 text-center">Utkast i Vaskeri</th>
<th className="pb-4">Sist Vasket</th>
</>
)}
2026-03-05 09:25:15 +01:00
<th className="pb-4 text-right pr-4">Handling</th>
</tr>
</thead>
2026-03-12 13:39:10 +01:00
<tbody className="text-sm font-bold text-[#11280f]">
2026-03-12 13:39:10 +01:00
{filteredFacilities.map((f: any) => {
const hasDraft = f.membership_draft && Object.keys(f.membership_draft).length > 0;
return (
<tr key={f.id} className={`border-b border-gray-50 group transition-colors ${hasDraft && activeTab === 'medlemskap' ? 'bg-[#8bc34a]/5' : 'hover:bg-gray-50/50'}`}>
<td className="py-6 pl-4 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-6 text-center text-xs font-mono text-gray-400">#{f.id}</td>
<td className="py-6 pr-6">
<div className="font-black text-base md:text-lg whitespace-nowrap">{f.name}</div>
<div className="text-[10px] text-[#7ca982] uppercase tracking-widest">{f.city}</div>
</td>
{activeTab === 'banestatus' ? (
<>
<td className="py-6 pr-4">
{/* HER BRUKER VI INLINE EDITOREN */}
<InlineEdit facilityId={f.id} field="scrape_status_url" initialValue={f.scrape_status_url} onSave={handleQuickEdit} />
<div className="text-[9px] font-mono text-gray-300 truncate max-w-[150px] mt-1" title={f.scrape_status_selector}>{f.scrape_status_selector}</div>
</td>
<td className="py-6 pr-4"><ScrapeMethodSelect facility={f} /></td>
<td className="py-6 text-gray-400 font-mono text-xs pr-4 whitespace-nowrap">
{f.status_updated_at ? new Date(f.status_updated_at).toLocaleDateString('nb-NO') : 'Aldri'}
</td>
<td className="py-6 pr-4">
<div className="flex flex-col gap-1">
{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-[9px] uppercase tracking-widest text-gray-400 truncate max-w-[80px]" title={cs.name}>{cs.name}</span>
<span className={`px-2 py-0.5 rounded-md text-[9px] font-black uppercase tracking-widest whitespace-nowrap ${badgeColor}`}>{cs.status || 'UKJENT'}</span>
</div>
)
})}
2026-03-05 05:18:03 +01:00
</div>
2026-03-12 13:39:10 +01:00
</td>
</>
) : (
<>
<td className="py-6 pr-4">
{/* HER BRUKER VI INLINE EDITOREN OGSÅ! */}
<InlineEdit facilityId={f.id} field="medlemskap_url" initialValue={f.medlemskap_url} onSave={handleQuickEdit} />
</td>
<td className="py-6 pr-4">
<div className="flex flex-col gap-1">
<span className="text-xs">Standard: <strong>{f.standard_medlemskap ? `${f.standard_medlemskap},-` : '---'}</strong></span>
<span className="text-xs text-gray-500">Rimeligste: <strong>{f.rimeligste_alternativ ? `${f.rimeligste_alternativ},-` : '---'}</strong></span>
</div>
</td>
<td className="py-6 pr-4 text-center">
{hasDraft ? <span className="px-3 py-1 bg-yellow-100 text-yellow-700 text-xs font-black uppercase tracking-widest rounded-xl animate-pulse">Ja</span> : <span className="text-gray-300">-</span>}
</td>
<td className="py-6 text-gray-400 font-mono text-xs pr-4 whitespace-nowrap">
{f.membership_updated_at ? new Date(f.membership_updated_at).toLocaleDateString('nb-NO') : 'Aldri'}
</td>
</>
)}
<td className="py-6 text-right pr-4">
<div className="flex flex-col gap-2 items-end">
{activeTab === 'banestatus' ? (
<button onClick={() => openEditModal(f)} className="bg-gray-100 px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest text-[#11280f] hover:bg-gray-200 transition-all whitespace-nowrap">Ekspert-skraper</button>
) : (
hasDraft ? (
<Link href="/admin/medlemskap" className="bg-yellow-100 text-yellow-800 px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest hover:bg-yellow-200 transition-all whitespace-nowrap"> til Vaskeriet</Link>
) : null
)}
<Link href={`/admin/rediger/${f.slug}`} className="bg-[#11280f] px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest text-white hover:bg-[#8bc34a] transition-all whitespace-nowrap text-center">Rediger alt</Link>
</div>
</td>
</tr>
);
})}
</tbody>
2026-03-06 09:39:32 +01:00
</table>
</div>
</div>
</main>
</div>
);
}