Før sammentrukne bokser i admin
This commit is contained in:
parent
3d54313394
commit
e2bcd7e21b
10 changed files with 530 additions and 99 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
BIN
2026-04-20_214654.png
Normal file
BIN
2026-04-20_214654.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
2026-04-20_214712.png
Normal file
BIN
2026-04-20_214712.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
257
backend/main.py
257
backend/main.py
|
|
@ -569,6 +569,10 @@ class QuickEditRequest(BaseModel):
|
|||
field: str
|
||||
value: str
|
||||
|
||||
|
||||
class FacilityVisibilityRequest(BaseModel):
|
||||
is_published: bool
|
||||
|
||||
class GreenfeeApproval(BaseModel):
|
||||
facility_id: int
|
||||
greenfee: List[dict]
|
||||
|
|
@ -893,6 +897,24 @@ def parse_optional_datetime(value: str | None) -> datetime | None:
|
|||
raise HTTPException(status_code=400, detail=f"Ugyldig datoformat: {value}") from exc
|
||||
|
||||
|
||||
def parse_optional_int(value: Any) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return int(value)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
trimmed = str(value).strip()
|
||||
if not trimmed:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(float(trimmed.replace(",", ".")))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def sanitize_hero_images(value: Any) -> list[dict[str, str]]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
|
|
@ -1318,6 +1340,7 @@ async def ensure_facility_columns(conn):
|
|||
"""Legger til nye facility-kolonner ved behov."""
|
||||
await conn.execute("""
|
||||
ALTER TABLE facilities
|
||||
ADD COLUMN IF NOT EXISTS is_published BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS footnote_updated_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS golfamore_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS golfpakker_url TEXT,
|
||||
|
|
@ -2114,6 +2137,7 @@ async def get_facilities():
|
|||
) w_data
|
||||
) as weather_forecast
|
||||
FROM facilities f
|
||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
||||
ORDER BY f.name ASC
|
||||
""")
|
||||
return [format_row(row) for row in rows]
|
||||
|
|
@ -2154,7 +2178,91 @@ async def get_facility(slug: str):
|
|||
ORDER BY day_offset ASC
|
||||
) w_data
|
||||
) as weather_forecast
|
||||
FROM facilities f WHERE f.slug = $1
|
||||
FROM facilities f
|
||||
WHERE f.slug = $1
|
||||
AND COALESCE(f.is_published, TRUE) = TRUE
|
||||
""", slug)
|
||||
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Golfanlegget ble ikke funnet")
|
||||
|
||||
return format_row(row)
|
||||
|
||||
|
||||
@app.get("/api/admin/facilities")
|
||||
async def get_admin_facilities():
|
||||
"""Henter alle golfanlegg for admin, også upubliserte."""
|
||||
async with app.state.pool.acquire() as conn:
|
||||
rows = await conn.fetch("""
|
||||
SELECT f.*, (
|
||||
SELECT jsonb_agg(cs) FROM (
|
||||
SELECT id, name, status FROM courses
|
||||
WHERE facility_id = f.id AND status != 'finnes_ingen_bane_to'
|
||||
ORDER BY is_main_course DESC, id ASC
|
||||
) cs
|
||||
) as course_statuses, (
|
||||
SELECT jsonb_agg(w_data ORDER BY w_data.day_offset ASC) FROM (
|
||||
SELECT
|
||||
forecast_date,
|
||||
day_offset,
|
||||
dry_all_day,
|
||||
dry_daylight,
|
||||
precip_mm,
|
||||
precip_probability_max,
|
||||
daylight_precip_mm,
|
||||
daylight_precip_probability_max,
|
||||
confidence,
|
||||
source_updated_at,
|
||||
source_expires_at,
|
||||
calculated_at
|
||||
FROM facility_weather_forecast
|
||||
WHERE facility_id = f.id
|
||||
ORDER BY day_offset ASC
|
||||
) w_data
|
||||
) as weather_forecast
|
||||
FROM facilities f
|
||||
ORDER BY f.name ASC
|
||||
""")
|
||||
return [format_row(row) for row in rows]
|
||||
|
||||
|
||||
@app.get("/api/admin/facilities/{slug}")
|
||||
async def get_admin_facility(slug: str):
|
||||
"""Henter full anleggsdetalj for admin, også når anlegget er upublisert."""
|
||||
async with app.state.pool.acquire() as conn:
|
||||
row = await conn.fetchrow("""
|
||||
SELECT f.*, (
|
||||
SELECT jsonb_agg(c_data) FROM (
|
||||
SELECT c.*, (
|
||||
SELECT jsonb_agg(h_data ORDER BY h_data.hole_number ASC)
|
||||
FROM (SELECT * FROM holes WHERE course_id = c.id) h_data
|
||||
) as holes
|
||||
FROM courses c
|
||||
WHERE c.facility_id = f.id
|
||||
ORDER BY c.is_main_course DESC, c.id ASC
|
||||
) c_data
|
||||
) as courses, (
|
||||
SELECT jsonb_agg(w_data ORDER BY w_data.day_offset ASC) FROM (
|
||||
SELECT
|
||||
forecast_date,
|
||||
day_offset,
|
||||
dry_all_day,
|
||||
dry_daylight,
|
||||
precip_mm,
|
||||
precip_probability_max,
|
||||
daylight_precip_mm,
|
||||
daylight_precip_probability_max,
|
||||
confidence,
|
||||
source_updated_at,
|
||||
source_expires_at,
|
||||
calculated_at
|
||||
FROM facility_weather_forecast
|
||||
WHERE facility_id = f.id
|
||||
ORDER BY day_offset ASC
|
||||
) w_data
|
||||
) as weather_forecast
|
||||
FROM facilities f
|
||||
WHERE f.slug = $1
|
||||
""", slug)
|
||||
|
||||
if not row:
|
||||
|
|
@ -2608,7 +2716,7 @@ async def update_facility_full(facility_id: int, request: Request):
|
|||
'guest_requirements', 'scrape_method', 'scrape_status_url',
|
||||
'social_links', 'footnote', 'cooperating_clubs', 'membership_draft', 'membership_updated_at',
|
||||
'greenfee_url', 'golfpakker_url', 'greenfee_draft', 'greenfee_updated_at', 'scrape_status_selector',
|
||||
'vtg_updated_at', 'vtg_draft', 'footnote_updated_at',
|
||||
'vtg_updated_at', 'vtg_draft', 'footnote_updated_at', 'is_published',
|
||||
'golfpakker_draft', 'golfpakker_updated_at'
|
||||
]
|
||||
|
||||
|
|
@ -2682,49 +2790,116 @@ async def update_facility_full(facility_id: int, request: Request):
|
|||
await conn.execute(query, *values)
|
||||
|
||||
# 2. OPPDATER BANER (COURSES) OG HULL (HOLES)
|
||||
courses = data.get('courses') or []
|
||||
for course in courses:
|
||||
if not course:
|
||||
continue
|
||||
course_id = course.get('id')
|
||||
if course_id:
|
||||
# Rens datoformat for PostgreSQL (håndterer Next.js date input)
|
||||
if 'courses' in data:
|
||||
submitted_courses = [course for course in (data.get('courses') or []) if course]
|
||||
normalized_courses: list[dict[str, Any]] = []
|
||||
|
||||
for index, course in enumerate(submitted_courses):
|
||||
normalized_course = dict(course)
|
||||
normalized_course['is_main_course'] = bool(course.get('is_main_course'))
|
||||
normalized_courses.append(normalized_course)
|
||||
|
||||
if normalized_courses:
|
||||
if not any(course['is_main_course'] for course in normalized_courses):
|
||||
normalized_courses[0]['is_main_course'] = True
|
||||
else:
|
||||
main_assigned = False
|
||||
for course in normalized_courses:
|
||||
if course['is_main_course'] and not main_assigned:
|
||||
main_assigned = True
|
||||
else:
|
||||
course['is_main_course'] = False
|
||||
|
||||
retained_course_ids: list[int] = []
|
||||
|
||||
for course in normalized_courses:
|
||||
course_id = course.get('id')
|
||||
holes = [hole for hole in (course.get('holes') or []) if hole]
|
||||
hole_count = len(holes) or None
|
||||
course_par = parse_optional_int(course.get('par'))
|
||||
course_length_meters = parse_optional_int(course.get('length_meters'))
|
||||
|
||||
valid_until_str = course.get('slope_valid_until')
|
||||
if valid_until_str == "" or valid_until_str is None:
|
||||
valid_until = None
|
||||
else:
|
||||
# Gjør om strengen til et ekte date-objekt for asyncpg
|
||||
try:
|
||||
date_part = valid_until_str.split('T')[0]
|
||||
date_part = str(valid_until_str).split('T')[0]
|
||||
valid_until = datetime.strptime(date_part, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
valid_until = None
|
||||
|
||||
await conn.execute("""
|
||||
UPDATE courses
|
||||
SET name=$1, par=$2, length_meters=$3, architect=$4,
|
||||
status=$5, is_main_course=$6, tee_boxes=$7::jsonb,
|
||||
slope_valid_until=$8
|
||||
WHERE id=$9 AND facility_id=$10
|
||||
""",
|
||||
course.get('name'), course.get('par'), course.get('length_meters'),
|
||||
course.get('architect'), course.get('status'), course.get('is_main_course'),
|
||||
json.dumps(course.get('tee_boxes') or {}), valid_until, course_id, facility_id)
|
||||
tee_boxes_json = json.dumps(course.get('tee_boxes') or {})
|
||||
|
||||
# 3. OPPDATER HULL PÅ BANEN (HOLES)
|
||||
holes = course.get('holes') or []
|
||||
if course_id:
|
||||
await conn.execute("""
|
||||
UPDATE courses
|
||||
SET name=$1, holes=$2, par=$3, length_meters=$4, architect=$5,
|
||||
status=$6, is_main_course=$7, tee_boxes=$8::jsonb,
|
||||
slope_valid_until=$9
|
||||
WHERE id=$10 AND facility_id=$11
|
||||
""",
|
||||
course.get('name'), hole_count, course_par, course_length_meters,
|
||||
course.get('architect'), course.get('status'), course.get('is_main_course'),
|
||||
tee_boxes_json, valid_until, course_id, facility_id)
|
||||
else:
|
||||
course_id = await conn.fetchval("""
|
||||
INSERT INTO courses (
|
||||
facility_id, name, holes, par, length_meters, architect,
|
||||
status, is_main_course, tee_boxes, slope_valid_until
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)
|
||||
RETURNING id
|
||||
""",
|
||||
facility_id, course.get('name'), hole_count, course_par, course_length_meters,
|
||||
course.get('architect'), course.get('status'), course.get('is_main_course'),
|
||||
tee_boxes_json, valid_until)
|
||||
|
||||
retained_course_ids.append(int(course_id))
|
||||
|
||||
retained_hole_ids: list[int] = []
|
||||
for hole in holes:
|
||||
if not hole:
|
||||
continue
|
||||
hole_id = hole.get('id')
|
||||
hole_number = parse_optional_int(hole.get('hole_number'))
|
||||
hole_par = parse_optional_int(hole.get('par'))
|
||||
hole_hcp_index = parse_optional_int(hole.get('hcp_index'))
|
||||
lengths_json = json.dumps(hole.get('lengths') or {})
|
||||
if hole_id:
|
||||
await conn.execute("""
|
||||
UPDATE holes
|
||||
SET par=$1, hcp_index=$2, lengths=$3::jsonb
|
||||
WHERE id=$4 AND course_id=$5
|
||||
SET hole_number=$1, par=$2, hcp_index=$3, lengths=$4::jsonb
|
||||
WHERE id=$5 AND course_id=$6
|
||||
""",
|
||||
hole.get('par'), hole.get('hcp_index'),
|
||||
json.dumps(hole.get('lengths') or {}), hole_id, course_id)
|
||||
hole_number, hole_par, hole_hcp_index,
|
||||
lengths_json, hole_id, course_id)
|
||||
else:
|
||||
hole_id = await conn.fetchval("""
|
||||
INSERT INTO holes (course_id, hole_number, par, hcp_index, lengths)
|
||||
VALUES ($1, $2, $3, $4, $5::jsonb)
|
||||
RETURNING id
|
||||
""",
|
||||
course_id, hole_number, hole_par, hole_hcp_index,
|
||||
lengths_json)
|
||||
|
||||
retained_hole_ids.append(int(hole_id))
|
||||
|
||||
if retained_hole_ids:
|
||||
await conn.execute(
|
||||
"DELETE FROM holes WHERE course_id = $1 AND NOT (id = ANY($2::int[]))",
|
||||
course_id,
|
||||
retained_hole_ids,
|
||||
)
|
||||
else:
|
||||
await conn.execute("DELETE FROM holes WHERE course_id = $1", course_id)
|
||||
|
||||
if retained_course_ids:
|
||||
await conn.execute(
|
||||
"DELETE FROM courses WHERE facility_id = $1 AND NOT (id = ANY($2::int[]))",
|
||||
facility_id,
|
||||
retained_course_ids,
|
||||
)
|
||||
else:
|
||||
await conn.execute("DELETE FROM courses WHERE facility_id = $1", facility_id)
|
||||
|
||||
extra_paths = ["/golfbaner"]
|
||||
if changed_field_names & membership_fields:
|
||||
|
|
@ -2737,6 +2912,30 @@ async def update_facility_full(facility_id: int, request: Request):
|
|||
)
|
||||
return {"status": "success", "message": "Anlegg, baner og scorekort ble oppdatert."}
|
||||
|
||||
|
||||
@app.delete("/api/admin/facilities/{facility_id}")
|
||||
async def delete_facility(facility_id: int):
|
||||
"""Sletter et anlegg permanent med tilhørende baner og hull."""
|
||||
async with app.state.pool.acquire() as conn:
|
||||
deleted = await conn.fetchrow(
|
||||
"DELETE FROM facilities WHERE id = $1 RETURNING slug, name",
|
||||
facility_id,
|
||||
)
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Golfanlegget ble ikke funnet")
|
||||
|
||||
deleted_slug = str(deleted["slug"] or "").strip()
|
||||
schedule_indexnow_submission(
|
||||
collect_facility_indexnow_urls([deleted_slug], extra_paths=["/golfbaner", "/medlemskap", "/vtg"]),
|
||||
reason="facility delete",
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"{deleted['name']} ble slettet.",
|
||||
"slug": deleted_slug,
|
||||
}
|
||||
|
||||
# --- NYTT ADMIN ENDPOINT: KJØRER SKRAPEREN FOR VALGTE IDER ---
|
||||
@app.get("/api/admin/scrape-jobs")
|
||||
async def get_scrape_jobs(job_type: Optional[str] = Query(default=None), limit: int = Query(default=10, ge=1, le=50)):
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export default function AdminArticlesPage() {
|
|||
};
|
||||
|
||||
const loadFacilities = async () => {
|
||||
const response = await fetch(`${API_URL}/facilities`, { credentials: "include" });
|
||||
const response = await adminFetch(`${API_URL}/admin/facilities`, { credentials: "include" });
|
||||
const data = await response.json();
|
||||
const mapped = Array.isArray(data)
|
||||
? data
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ export default function AdminDashboard() {
|
|||
const [dismissedLatestJobKeys, setDismissedLatestJobKeys] = useState<Partial<Record<AdminTab, string>>>({});
|
||||
|
||||
const fetchFacilities = () => {
|
||||
fetch(`${API_URL}/facilities`)
|
||||
adminFetch(`${API_URL}/admin/facilities`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setFacilities(Array.isArray(data) ? data : []);
|
||||
|
|
@ -1813,6 +1813,11 @@ export default function AdminDashboard() {
|
|||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-2xl font-black tracking-tight text-[#11280f]">{f.name}</h3>
|
||||
<span className="rounded-xl bg-white px-3 py-1 text-[10px] font-black uppercase tracking-[0.18em] text-gray-400 shadow-sm">ID {f.id}</span>
|
||||
{f.is_published === false && (
|
||||
<span className="rounded-xl bg-amber-100 px-3 py-1 text-[10px] font-black uppercase tracking-[0.18em] text-amber-800">
|
||||
Skjult
|
||||
</span>
|
||||
)}
|
||||
{isHighlighted && (
|
||||
<span className="rounded-xl bg-[#8bc34a] px-3 py-1 text-[10px] font-black uppercase tracking-[0.18em] text-white">
|
||||
Trenger oppmerksomhet
|
||||
|
|
@ -2163,7 +2168,14 @@ export default function AdminDashboard() {
|
|||
<td className={`py-6 pl-4 w-10 sticky left-0 z-10 ${isHighlighted ? 'bg-[#edf6e3]' : 'bg-white group-hover:bg-gray-50/50'}`}><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 sticky left-[56px] z-10 ${isHighlighted ? 'bg-[#edf6e3]' : 'bg-white group-hover:bg-gray-50/50'}`}>#{f.id}</td>
|
||||
<td className={`py-6 pr-6 sticky left-[104px] z-10 min-w-[220px] ${isHighlighted ? 'bg-[#edf6e3]' : 'bg-white group-hover:bg-gray-50/50'}`}>
|
||||
<div className="font-black text-base md:text-lg whitespace-nowrap">{f.name}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-black text-base md:text-lg whitespace-nowrap">{f.name}</div>
|
||||
{f.is_published === false && (
|
||||
<span className="rounded-lg bg-amber-100 px-2 py-1 text-[10px] font-black uppercase tracking-widest text-amber-800">
|
||||
Skjult
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] text-[#7ca982] uppercase tracking-widest">{f.city}</div>
|
||||
</td>
|
||||
|
||||
|
|
|
|||
|
|
@ -157,17 +157,21 @@ const ScorecardBuilder = ({ course, onChange }: { course: any, onChange: (c: any
|
|||
const herrer = course.tee_boxes?.herrer || [];
|
||||
const damer = course.tee_boxes?.damer || [];
|
||||
const initialTees = { herrer: {} as any, damer: {} as any };
|
||||
activeKeys.forEach((key, idx) => {
|
||||
initialTees.herrer[key] = herrer[idx] || { navn_utslag: '', baneverdi: '', slopeverdi: '' };
|
||||
initialTees.damer[key] = damer[idx] || { navn_utslag_damer: '', baneverdi_damer: '', slopeverdi_damer: '' };
|
||||
const herrerAreCompact = herrer.length > 0 && herrer.length < ALL_KEYS.length;
|
||||
const damerAreCompact = damer.length > 0 && damer.length < ALL_KEYS.length;
|
||||
|
||||
ALL_KEYS.forEach((key, idx) => {
|
||||
const activeIdx = activeKeys.indexOf(key);
|
||||
initialTees.herrer[key] = (herrerAreCompact && activeIdx >= 0 ? herrer[activeIdx] : herrer[idx]) || { navn_utslag: '', baneverdi: '', slopeverdi: '' };
|
||||
initialTees.damer[key] = (damerAreCompact && activeIdx >= 0 ? damer[activeIdx] : damer[idx]) || { navn_utslag_damer: '', baneverdi_damer: '', slopeverdi_damer: '' };
|
||||
});
|
||||
return initialTees;
|
||||
});
|
||||
|
||||
const syncToParent = (newHoles: any[], newKeys: string[], newTees: any) => {
|
||||
const updatedTeeBoxes = {
|
||||
herrer: newKeys.map(k => newTees.herrer[k] || {}),
|
||||
damer: newKeys.map(k => newTees.damer[k] || {})
|
||||
herrer: ALL_KEYS.map(k => newTees.herrer[k] || {}),
|
||||
damer: ALL_KEYS.map(k => newTees.damer[k] || {})
|
||||
};
|
||||
onChange({
|
||||
...course,
|
||||
|
|
@ -401,9 +405,14 @@ const getMediaFieldLabel = (field: string) => {
|
|||
|
||||
export default function EditFacilityClient({ initialData, allFacilities }: { initialData: any, allFacilities: any[] }) {
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState(initialData);
|
||||
const [formData, setFormData] = useState({
|
||||
...initialData,
|
||||
is_published: initialData?.is_published !== false,
|
||||
courses: Array.isArray(initialData?.courses) ? initialData.courses : [],
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState('generelt');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingFacility, setDeletingFacility] = useState(false);
|
||||
const [mediaFeedback, setMediaFeedback] = useState("");
|
||||
const [uploadingTarget, setUploadingTarget] = useState<string | null>(null);
|
||||
const mainImageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
|
@ -423,6 +432,60 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
setFormData((prev: any) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const updateCourses = (updater: (courses: any[]) => any[]) => {
|
||||
const nextCourses = updater(Array.isArray(formData.courses) ? formData.courses : []);
|
||||
handleChange('courses', nextCourses);
|
||||
};
|
||||
|
||||
const createEmptyCourse = () => {
|
||||
const existingCourses = Array.isArray(formData.courses) ? formData.courses : [];
|
||||
return {
|
||||
_clientId: `course-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: `Ny bane ${existingCourses.length + 1}`,
|
||||
status: 'ukjent',
|
||||
par: '',
|
||||
length_meters: '',
|
||||
architect: '',
|
||||
is_main_course: existingCourses.length === 0,
|
||||
slope_valid_until: '',
|
||||
tee_boxes: { herrer: [], damer: [] },
|
||||
holes: Array.from({ length: 18 }, (_, index) => ({
|
||||
hole_number: index + 1,
|
||||
par: '',
|
||||
hcp_index: '',
|
||||
lengths: {},
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const handleAddCourse = () => {
|
||||
updateCourses((courses) => [...courses, createEmptyCourse()]);
|
||||
};
|
||||
|
||||
const handleRemoveCourse = (index: number) => {
|
||||
const courses = Array.isArray(formData.courses) ? formData.courses : [];
|
||||
const course = courses[index];
|
||||
const confirmed = window.confirm(`Slette banen "${course?.name || 'uten navn'}"?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
updateCourses((currentCourses) => {
|
||||
const nextCourses = currentCourses.filter((_, courseIndex) => courseIndex !== index);
|
||||
if (nextCourses.length > 0 && !nextCourses.some((entry) => entry?.is_main_course)) {
|
||||
nextCourses[0] = { ...nextCourses[0], is_main_course: true };
|
||||
}
|
||||
return nextCourses;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetMainCourse = (index: number) => {
|
||||
updateCourses((courses) =>
|
||||
courses.map((course, courseIndex) => ({
|
||||
...course,
|
||||
is_main_course: courseIndex === index,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const galleryImages = normalizeStringList(formData.gallery);
|
||||
|
||||
const setGalleryImages = (images: string[]) => {
|
||||
|
|
@ -536,6 +599,30 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleDeleteFacility = async () => {
|
||||
const confirmed = window.confirm(`Slette anlegget "${initialData.name}" permanent? Dette fjerner også baner og hull.`);
|
||||
if (!confirmed) return;
|
||||
|
||||
setDeletingFacility(true);
|
||||
try {
|
||||
const response = await adminFetch(`/api/admin/facilities/${initialData.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
alert("Noe gikk galt under sletting.");
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/admin');
|
||||
router.refresh();
|
||||
} catch {
|
||||
alert("Nettverksfeil under sletting.");
|
||||
} finally {
|
||||
setDeletingFacility(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'generelt', label: 'Generelt' },
|
||||
{ id: 'lokasjon', label: 'Lokasjon & Kontakt' },
|
||||
|
|
@ -561,24 +648,43 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
<Link href="/admin" className="text-sm font-bold text-gray-500 hover:text-[#8bc34a] mb-2 block">← Tilbake til oversikten</Link>
|
||||
<h1 className="text-4xl font-black text-[#11280f]">
|
||||
Rediger:{" "}
|
||||
<Link
|
||||
href={`/golfbaner/${initialData.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#8bc34a]"
|
||||
title="Åpne anleggssiden i ny fane"
|
||||
>
|
||||
{initialData.name}
|
||||
</Link>
|
||||
{formData.is_published ? (
|
||||
<Link
|
||||
href={`/golfbaner/${initialData.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#8bc34a]"
|
||||
title="Åpne anleggssiden i ny fane"
|
||||
>
|
||||
{initialData.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-[#8bc34a]">{initialData.name}</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="mt-3 flex flex-wrap items-center gap-3 text-xs font-black uppercase tracking-widest">
|
||||
<span className={`rounded-xl px-3 py-2 ${formData.is_published ? 'bg-[#8bc34a] text-white' : 'bg-amber-100 text-amber-800'}`}>
|
||||
{formData.is_published ? 'Publisert' : 'Skjult fra offentligheten'}
|
||||
</span>
|
||||
<span className="rounded-xl bg-gray-100 px-3 py-2 text-gray-500">Slug: {initialData.slug}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3 md:w-auto md:flex-row">
|
||||
<button
|
||||
onClick={handleDeleteFacility}
|
||||
disabled={deletingFacility}
|
||||
className="btn btn-lg btn-danger w-full md:w-auto disabled:opacity-50"
|
||||
>
|
||||
{deletingFacility ? "Sletter..." : "Slett anlegg"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn btn-lg btn-primary w-full md:w-auto disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Lagrer..." : "Lagre endringer"}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn btn-lg btn-primary w-full md:w-auto disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Lagrer..." : "Lagre endringer"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-10">
|
||||
|
|
@ -599,6 +705,24 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
<div className="w-full md:w-3/4">
|
||||
{activeTab === 'generelt' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8">
|
||||
<div className="col-span-1 md:col-span-2 mb-8 rounded-[2rem] border border-gray-200 bg-gray-50 p-6 shadow-sm">
|
||||
<p className="text-xs font-black uppercase tracking-widest text-gray-500">Publisering</p>
|
||||
<div className="mt-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-lg font-black text-[#11280f]">{formData.is_published ? 'Anlegget er publisert' : 'Anlegget er skjult'}</p>
|
||||
<p className="mt-1 text-sm text-gray-500">Skjulte anlegg forsvinner fra offentlige lister og anleggssiden, men forblir tilgjengelige i admin.</p>
|
||||
</div>
|
||||
<label className="inline-flex items-center gap-3 rounded-2xl bg-white px-4 py-3 shadow-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(formData.is_published)}
|
||||
onChange={(e) => handleChange('is_published', e.target.checked)}
|
||||
className="h-5 w-5 accent-[#8bc34a]"
|
||||
/>
|
||||
<span className="text-sm font-black uppercase tracking-widest text-[#11280f]">Publisert</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 md:col-span-2 flex flex-col gap-2 mb-8">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-gray-600">Anleggsnavn</label>
|
||||
<input className="p-4 rounded-2xl border-2 border-gray-300 bg-white text-black text-base font-bold shadow-sm focus:border-[#8bc34a] outline-none" value={getValue('name', 'text')} onChange={e => handleChange('name', e.target.value)} />
|
||||
|
|
@ -1008,35 +1132,60 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
|
||||
{activeTab === 'baner' && (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="bg-[#f1f7ed] p-6 rounded-2xl border-2 border-[#7ca982] mb-4">
|
||||
<h3 className="font-black text-[#11280f] text-lg uppercase tracking-widest mb-2">Baner og Scorekort</h3>
|
||||
<p className="text-sm text-gray-800 font-medium">Bruk det interaktive skjemaet under for å redigere lengder, par og utslag.</p>
|
||||
<div className="flex flex-col gap-4 rounded-2xl border-2 border-[#7ca982] bg-[#f1f7ed] p-6">
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-black uppercase tracking-widest text-[#11280f]">Baner og Scorekort</h3>
|
||||
<p className="text-sm font-medium text-gray-800">Bruk det interaktive skjemaet under for å redigere lengder, par og utslag. Nye baner lagres sammen med anlegget og blir behandlet som egne baner i detaljvisningen.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="rounded-xl bg-white px-4 py-3 text-xs font-black uppercase tracking-widest text-gray-500 shadow-sm">{formData.courses?.length || 0} baner</span>
|
||||
<button onClick={handleAddCourse} className="btn btn-md btn-secondary w-full sm:w-auto">+ Legg til bane</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.courses?.map((course: any, cIdx: number) => (
|
||||
<div key={course.id || cIdx} className="bg-gray-100 p-8 rounded-[2rem] border-2 border-gray-200 shadow-sm mb-8">
|
||||
<div key={course.id || course._clientId || cIdx} className="bg-gray-100 p-8 rounded-[2rem] border-2 border-gray-200 shadow-sm mb-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4 border-b-2 border-gray-200 pb-4">
|
||||
<h4 className="text-2xl font-black text-black">{course.name}</h4>
|
||||
<span className={`px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest ${course.is_main_course ? 'bg-[#8bc34a] text-white shadow-md' : 'bg-gray-300 text-gray-700'}`}>
|
||||
{course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
|
||||
</span>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-2xl font-black text-black">{course.name}</h4>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="inline-flex items-center gap-3 rounded-xl bg-white px-4 py-2 shadow-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="main-course"
|
||||
checked={Boolean(course.is_main_course)}
|
||||
onChange={() => handleSetMainCourse(cIdx)}
|
||||
className="h-4 w-4 accent-[#8bc34a]"
|
||||
/>
|
||||
<span className="text-xs font-black uppercase tracking-widest text-[#11280f]">Hovedbane</span>
|
||||
</label>
|
||||
<span className={`px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest ${course.is_main_course ? 'bg-[#8bc34a] text-white shadow-md' : 'bg-gray-300 text-gray-700'}`}>
|
||||
{course.is_main_course ? 'Hovedbane' : 'Sekundærbane'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => handleRemoveCourse(cIdx)} className="btn btn-md btn-danger w-full md:w-auto">Slett bane</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8">
|
||||
<div className="flex flex-col gap-2 mb-6">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-gray-600">Banenavn</label>
|
||||
<input className="p-4 rounded-2xl border-2 border-gray-300 focus:border-[#8bc34a] outline-none font-bold text-black bg-white text-base shadow-sm" value={course.name || ""} onChange={e => {
|
||||
const newCourses = [...formData.courses];
|
||||
newCourses[cIdx] = {...course, name: e.target.value};
|
||||
handleChange('courses', newCourses);
|
||||
updateCourses((courses) => {
|
||||
const nextCourses = [...courses];
|
||||
nextCourses[cIdx] = {...course, name: e.target.value};
|
||||
return nextCourses;
|
||||
});
|
||||
}} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mb-6">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-gray-600">Status</label>
|
||||
<select className="p-4 rounded-2xl border-2 border-gray-300 focus:border-[#8bc34a] outline-none font-bold text-black bg-white text-base shadow-sm" value={course.status || "ukjent"} onChange={e => {
|
||||
const newCourses = [...formData.courses];
|
||||
newCourses[cIdx] = {...course, status: e.target.value};
|
||||
handleChange('courses', newCourses);
|
||||
updateCourses((courses) => {
|
||||
const nextCourses = [...courses];
|
||||
nextCourses[cIdx] = {...course, status: e.target.value};
|
||||
return nextCourses;
|
||||
});
|
||||
}}>
|
||||
<option value="aapen">🟢 Åpen</option>
|
||||
<option value="aapen_med_vintergreener">🟡 Vintergreener</option>
|
||||
|
|
@ -1049,17 +1198,21 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
<div className="flex flex-col gap-2 mb-6">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-gray-600">Total Par (Bane)</label>
|
||||
<input type="number" className="p-4 rounded-2xl border-2 border-gray-300 focus:border-[#8bc34a] outline-none font-bold text-black bg-white text-base shadow-sm" value={course.par || ""} onChange={e => {
|
||||
const newCourses = [...formData.courses];
|
||||
newCourses[cIdx] = {...course, par: Number(e.target.value)};
|
||||
handleChange('courses', newCourses);
|
||||
updateCourses((courses) => {
|
||||
const nextCourses = [...courses];
|
||||
nextCourses[cIdx] = {...course, par: Number(e.target.value)};
|
||||
return nextCourses;
|
||||
});
|
||||
}} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mb-6">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-gray-600">Utløpsdato Slope</label>
|
||||
<input type="date" className="p-4 rounded-2xl border-2 border-gray-300 focus:border-[#8bc34a] outline-none font-bold text-black bg-white text-base shadow-sm" value={course.slope_valid_until ? course.slope_valid_until.split('T')[0] : ""} onChange={e => {
|
||||
const newCourses = [...formData.courses];
|
||||
newCourses[cIdx] = {...course, slope_valid_until: e.target.value};
|
||||
handleChange('courses', newCourses);
|
||||
updateCourses((courses) => {
|
||||
const nextCourses = [...courses];
|
||||
nextCourses[cIdx] = {...course, slope_valid_until: e.target.value};
|
||||
return nextCourses;
|
||||
});
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1068,9 +1221,11 @@ export default function EditFacilityClient({ initialData, allFacilities }: { ini
|
|||
<ScorecardBuilder
|
||||
course={course}
|
||||
onChange={(updatedCourse) => {
|
||||
const newCourses = [...formData.courses];
|
||||
newCourses[cIdx] = updatedCourse;
|
||||
handleChange('courses', newCourses);
|
||||
updateCourses((courses) => {
|
||||
const nextCourses = [...courses];
|
||||
nextCourses[cIdx] = updatedCourse;
|
||||
return nextCourses;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { API_URL } from "@/config/constants";
|
||||
import EditFacilityClient from "./EditFacilityClient";
|
||||
|
||||
export default async function EditFacilityPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const cookieHeader = (await cookies()).toString();
|
||||
|
||||
// Henter anlegget vi skal redigere
|
||||
const res = await fetch(`${API_URL}/facilities/${slug}`, { cache: 'no-store' });
|
||||
const res = await fetch(`${API_URL}/admin/facilities/${slug}`, {
|
||||
cache: 'no-store',
|
||||
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
||||
});
|
||||
const facility = await res.json();
|
||||
|
||||
// Henter ALLE anlegg slik at vi kan bygge lister for samarbeid og arkitekter
|
||||
const allRes = await fetch(`${API_URL}/facilities`, { cache: 'no-store' });
|
||||
const allRes = await fetch(`${API_URL}/admin/facilities`, {
|
||||
cache: 'no-store',
|
||||
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
||||
});
|
||||
const allFacilities = await allRes.json();
|
||||
|
||||
if (!facility || facility.error) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import { useState } from 'react';
|
||||
import { STATUS_MAP } from "@/config/constants";
|
||||
|
||||
type Gender = 'herrer' | 'damer';
|
||||
|
||||
// Designerens definisjon av fargetemaer - Nå med kraftigere tints for kolonnene
|
||||
const getTeeTheme = (label: string) => {
|
||||
const name = label.toLowerCase();
|
||||
|
|
@ -30,7 +32,7 @@ const getTeeTheme = (label: string) => {
|
|||
|
||||
export default function CourseDisplay({ course, courseDisplayName = "" }: { course: any; courseDisplayName?: string }) {
|
||||
const [hcp, setHcp] = useState("15.0");
|
||||
const [gender, setGender] = useState<'herrer' | 'damer'>('herrer');
|
||||
const [gender, setGender] = useState<Gender>('herrer');
|
||||
const [selectedTeeIndex, setSelectedTeeIndex] = useState(0);
|
||||
|
||||
const allHoles = course.holes || [];
|
||||
|
|
@ -39,18 +41,44 @@ export default function CourseDisplay({ course, courseDisplayName = "" }: { cour
|
|||
const hasInHoles = holesIn.length > 0;
|
||||
|
||||
const lengthKeys = ['lengst', 'lang', 'mellomlang', 'mellomkort', 'kort', 'kortest'];
|
||||
const activeLengthKeys = lengthKeys.filter((key) => allHoles.some((h: any) => h.lengths?.[key]));
|
||||
const availableTees = course.tee_boxes?.[gender] || [];
|
||||
const fallbackTees = course.tee_boxes?.[gender === 'damer' ? 'herrer' : 'damer'] || [];
|
||||
|
||||
const activeColumns = lengthKeys
|
||||
.filter(k => allHoles.some((h: any) => h.lengths?.[k]))
|
||||
.map((key, idx) => {
|
||||
const info = availableTees[idx];
|
||||
const label = info?.navn_utslag || info?.navn_utslag_damer || key.toUpperCase();
|
||||
return { key, label, theme: getTeeTheme(label) };
|
||||
});
|
||||
const getTeeForColumn = (tees: any[], teeIndex: number, activePosition: number) => {
|
||||
if (!Array.isArray(tees) || tees.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Older/broken saves may store only active tees in sequence instead of the full 6-slot layout.
|
||||
if (tees.length < lengthKeys.length) {
|
||||
return tees[activePosition];
|
||||
}
|
||||
|
||||
return tees[teeIndex];
|
||||
};
|
||||
|
||||
const getTeeLabel = (tee: any, fallbackTee: any, fallback: string) => {
|
||||
const primaryLabel = gender === 'damer' ? tee?.navn_utslag_damer : tee?.navn_utslag;
|
||||
const secondaryLabel = gender === 'damer' ? fallbackTee?.navn_utslag : fallbackTee?.navn_utslag_damer;
|
||||
const tertiaryLabel = gender === 'damer' ? tee?.navn_utslag : tee?.navn_utslag_damer;
|
||||
const quaternaryLabel = gender === 'damer' ? fallbackTee?.navn_utslag_damer : fallbackTee?.navn_utslag;
|
||||
return String(primaryLabel || secondaryLabel || tertiaryLabel || quaternaryLabel || fallback).trim();
|
||||
};
|
||||
|
||||
const activeColumns = activeLengthKeys.map((key, activePosition) => {
|
||||
const teeIndex = lengthKeys.indexOf(key);
|
||||
const tee = getTeeForColumn(availableTees, teeIndex, activePosition);
|
||||
const fallbackTee = getTeeForColumn(fallbackTees, teeIndex, activePosition);
|
||||
const label = getTeeLabel(tee, fallbackTee, key[0].toUpperCase() + key.slice(1));
|
||||
return { key, teeIndex, activePosition, label, theme: getTeeTheme(label) };
|
||||
});
|
||||
|
||||
// Kalkulering av SpH
|
||||
const activeTee = availableTees[selectedTeeIndex];
|
||||
const selectedColumn = activeColumns.find((column) => column.teeIndex === selectedTeeIndex) || activeColumns[0] || null;
|
||||
const activeTee = selectedColumn
|
||||
? getTeeForColumn(availableTees, selectedColumn.teeIndex, selectedColumn.activePosition)
|
||||
: undefined;
|
||||
let playingHandicap = 0;
|
||||
|
||||
if (activeTee && hcp) {
|
||||
|
|
@ -69,8 +97,13 @@ export default function CourseDisplay({ course, courseDisplayName = "" }: { cour
|
|||
|
||||
const sumPar = (holes: any[]) => holes.reduce((acc, h) => acc + (h.par || 0), 0);
|
||||
const sumLen = (holes: any[], key: string) => holes.reduce((acc, h) => acc + (h.lengths?.[key] || 0), 0);
|
||||
const selectedColumn = activeColumns[selectedTeeIndex] || activeColumns[0] || null;
|
||||
const selectedTeeLabel = selectedColumn?.label || activeTee?.navn_utslag || activeTee?.navn_utslag_damer || 'Valgt utslag';
|
||||
const selectedTeeLabel = activeTee
|
||||
? getTeeLabel(
|
||||
activeTee,
|
||||
selectedColumn ? getTeeForColumn(fallbackTees, selectedColumn.teeIndex, selectedColumn.activePosition) : undefined,
|
||||
selectedColumn?.label || 'Valgt utslag'
|
||||
)
|
||||
: 'Valgt utslag';
|
||||
|
||||
// Formater utløpsdato
|
||||
const slopeExpiry = course.slope_valid_until
|
||||
|
|
@ -147,13 +180,36 @@ export default function CourseDisplay({ course, courseDisplayName = "" }: { cour
|
|||
|
||||
<div className="grid w-full min-w-0 max-w-full grid-cols-1 gap-3 rounded-[1.6rem] border border-gray-100 bg-gray-50 p-4 sm:grid-cols-2 md:flex md:w-auto md:max-w-none md:items-center md:gap-6 md:rounded-[2rem] md:p-6">
|
||||
<div className="flex min-w-0 flex-col"><span className="ml-1 text-[9px] font-black uppercase text-[#7ca982]">Kjønn</span>
|
||||
<select value={gender} onChange={e => { setGender(e.target.value as any); setSelectedTeeIndex(0); }} className="w-full min-w-0 truncate border-b-2 border-[#7ca982]/30 bg-transparent pb-1 pr-6 text-[#11280f] font-black outline-none cursor-pointer">
|
||||
<select value={gender} onChange={e => {
|
||||
const nextGender = e.target.value as Gender;
|
||||
const nextTees = course.tee_boxes?.[nextGender] || [];
|
||||
const nextFallbackTees = course.tee_boxes?.[nextGender === 'damer' ? 'herrer' : 'damer'] || [];
|
||||
const nextFirstTeeIndex = activeLengthKeys.findIndex((key, activePosition) =>
|
||||
Boolean(
|
||||
String(
|
||||
nextGender === 'damer'
|
||||
? getTeeForColumn(nextTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag_damer ||
|
||||
getTeeForColumn(nextFallbackTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag ||
|
||||
getTeeForColumn(nextTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag ||
|
||||
getTeeForColumn(nextFallbackTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag_damer ||
|
||||
""
|
||||
: getTeeForColumn(nextTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag ||
|
||||
getTeeForColumn(nextFallbackTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag_damer ||
|
||||
getTeeForColumn(nextTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag_damer ||
|
||||
getTeeForColumn(nextFallbackTees, lengthKeys.indexOf(key), activePosition)?.navn_utslag ||
|
||||
""
|
||||
).trim()
|
||||
)
|
||||
);
|
||||
setGender(nextGender);
|
||||
setSelectedTeeIndex(nextFirstTeeIndex >= 0 ? lengthKeys.indexOf(activeLengthKeys[nextFirstTeeIndex]) : 0);
|
||||
}} className="w-full min-w-0 truncate border-b-2 border-[#7ca982]/30 bg-transparent pb-1 pr-6 text-[#11280f] font-black outline-none cursor-pointer">
|
||||
<option value="herrer">HERRER</option><option value="damer">DAMER</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col"><span className="ml-1 text-[9px] font-black uppercase text-[#7ca982]">Utslag</span>
|
||||
<select value={selectedTeeIndex} onChange={e => setSelectedTeeIndex(Number(e.target.value))} className="w-full min-w-0 truncate border-b-2 border-[#7ca982]/30 bg-transparent pb-1 pr-6 text-[#11280f] font-black outline-none cursor-pointer">
|
||||
{availableTees.map((t: any, i: number) => (<option key={i} value={i}>{t.navn_utslag || t.navn_utslag_damer}</option>))}
|
||||
{activeColumns.map((col) => (<option key={col.teeIndex} value={col.teeIndex}>{col.label}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col"><span className="ml-1 text-[9px] font-black uppercase text-[#7ca982]">Ditt HCP</span>
|
||||
|
|
|
|||
|
|
@ -496,6 +496,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
<div className="space-y-4">
|
||||
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Drivingrange:</span><span className="text-right ml-4">{renderValue(amenities.drivingrange, 'Nei')}</span></div>
|
||||
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Nærspill:</span><span className="text-right ml-4">{renderValue(amenities.treningsgreen, 'Ja')}</span></div>
|
||||
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Pro:</span><span className="text-right ml-4">{renderValue(amenities.pro)}</span></div>
|
||||
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Proshop:</span><span className="text-right ml-4">{renderValue(amenities.proshop)}</span></div>
|
||||
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Kølleutleie:</span><span className="text-right ml-4">{renderValue(amenities.kolleutleie, 'Ja')}</span></div>
|
||||
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Bilutleie:</span><span className="text-right ml-4">{renderValue(amenities.bilutleie, 'Nei')}</span></div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue