- | handleSelectOne(f.id, e.target.checked)} /> |
- #{f.id} |
-
+ | handleSelectOne(f.id, e.target.checked)} /> |
+ #{f.id} |
+
{f.name}
{f.city}
|
@@ -580,7 +1172,7 @@ export default function AdminDashboard() {
>
)}
-
+ |
{activeTab === 'banestatus' && }
{activeTab === 'medlemskap' && hasMemDraft && Gå til Vaskeri}
diff --git a/frontend/src/app/admin/rediger/[slug]/EditFacilityClient.tsx b/frontend/src/app/admin/rediger/[slug]/EditFacilityClient.tsx
index 3904d16..59f7d31 100644
--- a/frontend/src/app/admin/rediger/[slug]/EditFacilityClient.tsx
+++ b/frontend/src/app/admin/rediger/[slug]/EditFacilityClient.tsx
@@ -2,6 +2,8 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
+import { adminFetch } from "@/config/adminFetch";
+import AdminMobileMenu from "@/components/AdminMobileMenu";
// KOMPONENT 1: MultiSelect for samarbeidende klubber
const MultiSelect = ({ label, options, selected, onChange }: { label: string, options: any[], selected: string[], onChange: (s: string[]) => void }) => {
@@ -60,9 +62,9 @@ const KeyValueEditor = ({ label, value, onChange }: { label: string, value: any,
{entries.map(([k, v]) => (
-
+
updateKey(k, e.target.value, v)}
@@ -73,7 +75,7 @@ const KeyValueEditor = ({ label, value, onChange }: { label: string, value: any,
value={String(v)}
onChange={e => updateVal(k, e.target.value)}
/>
-
+
))}
@@ -234,7 +236,79 @@ const ScorecardBuilder = ({ course, onChange }: { course: any, onChange: (c: any
))}
-
+
+
+ Herrer
+
+ {activeKeys.map(k => (
+
+ ))}
+
+
+
+
+ Damer
+
+ {activeKeys.map(k => (
+
+ ))}
+
+
+
+
+
+
+
+ Hull for hull
+ Hvert hull er et eget kort med par, hcp og lengder per aktiv utslagskolonne.
+
+ {holes.length} hull
+
+
+ {holes.map((h, idx) => (
+
+
+ Hull {h.hole_number}
+ {activeKeys.length} utslag
+
+
+
+ {activeKeys.map(k => (
+
+
+ updateHole(idx, 'lengths', e.target.value, k)} />
+
+ ))}
+
+
+ ))}
+
+
+
+
@@ -295,9 +369,9 @@ const ScorecardBuilder = ({ course, onChange }: { course: any, onChange: (c: any
-
-
-
+
+
+
);
@@ -326,7 +400,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
const handleSave = async () => {
setSaving(true);
try {
- const res = await fetch(`/api/admin/facilities/${initialData.id}/full`, {
+ const res = await adminFetch(`/api/admin/facilities/${initialData.id}/full`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
@@ -363,6 +437,7 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
return (
+
← Tilbake til oversikten
@@ -634,4 +709,4 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/app/admin/vtg/page.tsx b/frontend/src/app/admin/vtg/page.tsx
index 11303a1..de98679 100644
--- a/frontend/src/app/admin/vtg/page.tsx
+++ b/frontend/src/app/admin/vtg/page.tsx
@@ -1,6 +1,8 @@
"use client";
import { useState, useEffect } from 'react';
import { API_URL } from "@/config/constants";
+import { adminFetch } from "@/config/adminFetch";
+import AdminMobileMenu from "@/components/AdminMobileMenu";
import Link from 'next/link';
export default function VtgWasher() {
@@ -11,7 +13,7 @@ export default function VtgWasher() {
const fetchDrafts = () => {
setLoading(true);
- fetch(`${API_URL}/admin/vtg/drafts`)
+ adminFetch(`${API_URL}/admin/vtg/drafts`)
.then(res => res.json())
.then(data => {
const editableDrafts = data.map((f: any) => {
@@ -94,7 +96,7 @@ export default function VtgWasher() {
setSaving(true);
try {
- const res = await fetch(`${API_URL}/admin/vtg/approve-bulk`, {
+ const res = await adminFetch(`${API_URL}/admin/vtg/approve-bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ approvals: toApprove })
@@ -117,13 +119,14 @@ export default function VtgWasher() {
return (
-
+
+
← Tilbake til oversikten
VTG-Vaskeriet
Gå gjennom og godkjenn kursinformasjon for Veien til Golf.
-
@@ -140,14 +143,14 @@ export default function VtgWasher() {
Velg Alle
- {drafts.map(draft => (
-
+ {drafts.map((draft, index) => (
+
toggleOne(draft.id)} />
-
+
{draft.vtg_draft?.ai_begrunnelse && (
@@ -178,15 +181,15 @@ export default function VtgWasher() {
Fant ingen spesifikke kursdatoer.
) : (
draft.edit_datoer.map((row: any, idx: number) => (
-
- updateDateRow(draft.id, idx, 'dato', e.target.value)} placeholder="F.eks: 12.-14. mai" />
-
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/AdminMobileMenu.tsx b/frontend/src/components/AdminMobileMenu.tsx
new file mode 100755
index 0000000..8752b3f
--- /dev/null
+++ b/frontend/src/components/AdminMobileMenu.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { API_URL } from "@/config/constants";
+
+type AdminMobileMenuProps = {
+ onOpenTwoFactor?: () => void;
+};
+
+const NAV_ITEMS = [
+ { href: '/admin', label: 'Kontrollpanel', match: (pathname: string) => pathname === '/admin' },
+ { href: '/admin/medlemskap', label: 'Medlemskap', match: (pathname: string) => pathname.startsWith('/admin/medlemskap') },
+ { href: '/admin/greenfee', label: 'Greenfee', match: (pathname: string) => pathname.startsWith('/admin/greenfee') },
+ { href: '/admin/vtg', label: 'VTG', match: (pathname: string) => pathname.startsWith('/admin/vtg') },
+];
+
+export default function AdminMobileMenu({ onOpenTwoFactor }: AdminMobileMenuProps) {
+ const pathname = usePathname();
+ const [isOpen, setIsOpen] = useState(false);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsOpen(false);
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen]);
+
+ const handleLogout = async () => {
+ try {
+ await fetch(`${API_URL}/auth/logout`, {
+ method: 'POST',
+ credentials: 'include'
+ });
+ } finally {
+ window.location.href = '/';
+ }
+ };
+
+ return (
+ <>
+
+ setIsOpen(true)}
+ className="inline-flex items-center gap-3 rounded-2xl bg-[#11280f] px-5 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-white shadow-lg"
+ >
+ ☰
+ Adminmeny
+
+
+
+ {isOpen && (
+ {
+ if (event.target === event.currentTarget) {
+ setIsOpen(false);
+ }
+ }}
+ >
+
+
+
+ Adminmeny
+ TeeOff Admin
+
+ setIsOpen(false)}
+ className="inline-flex h-11 w-11 items-center justify-center rounded-2xl bg-white/10 text-xl font-black text-white hover:bg-white/20"
+ aria-label="Lukk adminmeny"
+ >
+ ×
+
+
+
+
+
+
+
+ Logg ut
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 48bf3de..9319346 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -20,7 +20,6 @@ export default function Header() {
Finn Bane
Medlemskap
Om oss
- Admin
{/* HAMBURGER (Mobil) */}
@@ -37,9 +36,8 @@ export default function Header() {
setIsOpen(false)} href="/" className="text-lg font-black uppercase text-[#11280f]">Hjem
setIsOpen(false)} href="/golfbaner" className="text-lg font-black uppercase text-[#11280f]">Finn Bane
setIsOpen(false)} href="/medlemskap" className="text-lg font-black uppercase text-[#11280f]">Medlemskap
- setIsOpen(false)} href="/logg-inn" className="text-[#ff5722] font-black uppercase">Admin Logg inn
)}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/ScrapeMethodSelect.tsx b/frontend/src/components/ScrapeMethodSelect.tsx
index 8b93e85..0656438 100644
--- a/frontend/src/components/ScrapeMethodSelect.tsx
+++ b/frontend/src/components/ScrapeMethodSelect.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from 'react';
+import { adminFetch } from "@/config/adminFetch";
// Tilpass interface til de dataene du allerede har i frontend
interface Facility {
@@ -23,7 +24,7 @@ export default function ScrapeMethodSelect({ facility }: { facility: Facility })
try {
// Husk å endre URL-en hvis API-et ditt ligger på et annet domene
- const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || ''}/api/admin/facilities/${facility.id}/scrape-settings`, {
+ const response = await adminFetch(`${process.env.NEXT_PUBLIC_API_URL || ''}/api/admin/facilities/${facility.id}/scrape-settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
@@ -68,4 +69,4 @@ export default function ScrapeMethodSelect({ facility }: { facility: Facility })
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/config/adminFetch.ts b/frontend/src/config/adminFetch.ts
new file mode 100755
index 0000000..57e368c
--- /dev/null
+++ b/frontend/src/config/adminFetch.ts
@@ -0,0 +1,10 @@
+export async function adminFetch(input: RequestInfo | URL, init?: RequestInit): Promise {
+ const response = await fetch(input, init);
+
+ if (response.status === 401 && typeof window !== 'undefined') {
+ window.location.href = '/admin/login';
+ throw new Error('ADMIN_UNAUTHORIZED');
+ }
+
+ return response;
+}
|