import React, { useEffect, useRef, useState } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, useMediaQuery } from '@mui/material'; import EditableAreaMap from './EditableAreaMap'; import './SiteGeometryModal.css'; export async function siteGeometryRequest(projectId, apiBaseUrl, method, body) { const token = localStorage.getItem('accessToken'); const response = await fetch(`${apiBaseUrl}/api/customer-projects/projects/${projectId}/site-geometry${method === 'POST' ? '/geocode' : ''}`, { method, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), 'Content-Type': 'application/json' }, ...(body ? { body: JSON.stringify(body) } : {}) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'Kortområdet kunne ikke hentes.'); return result; } const savedPoints = geometry => geometry?.areas?.[0]?.geometry.coordinates[0].slice(0,-1) || []; function preview(points) { if (points.length < 3) return 0; const r = Math.PI / 180; return Math.abs(points.reduce((sum,p,i) => { const next = points[(i+1)%points.length]; return sum + (next[0]-p[0])*r*(2+Math.sin(p[1]*r)+Math.sin(next[1]*r)); },0))*6371008.8**2/2; } export default function SiteGeometryModal({ projectId, apiBaseUrl = '', address = '', roof = false, onSave, onDelete, onClose, onManual, api }) { const pointList = useRef(null); const addressInput = useRef(null); const mobile = useMediaQuery('(max-width:600px)'); const [saved,setSaved] = useState(null); const [revision,setRevision] = useState(0); const [points,setPoints] = useState([]); const [center,setCenter] = useState([56,10]); const [query,setQuery] = useState(address); const [candidates,setCandidates] = useState([]); const [error,setError] = useState(''); const [loading,setLoading] = useState(true); const [loadFailed,setLoadFailed] = useState(false); const [loadAttempt,setLoadAttempt] = useState(0); const [busy,setBusy] = useState(false); const [drawing,setDrawing] = useState(true); const [deleteConfirm,setDeleteConfirm] = useState(false); const [status,setStatus] = useState(''); const request = (method,body) => api ? api(method,body) : siteGeometryRequest(projectId,apiBaseUrl,method,body); useEffect(() => { let active = true; const load = async () => { setLoading(true);setLoadFailed(false);setError(''); try { const result = await (api ? api('GET') : siteGeometryRequest(projectId,apiBaseUrl,'GET')); if (!active) return; const {geometry} = result; setRevision(result.revision ?? geometry?.revision ?? 0); setSaved(geometry);const next=savedPoints(geometry);setPoints(next); setDrawing(!next.length); if (next.length) setCenter([next[0][1],next[0][0]]); } catch(e) { if (active) {setError(e.message);setLoadFailed(true);} } finally { if (active) { setLoading(false); if (loadAttempt > 0) addressInput.current?.focus(); } } }; load(); return () => { active=false; }; },[projectId,apiBaseUrl,api,loadAttempt]); const unavailable = busy || loading || loadFailed; const dirty = JSON.stringify(points) !== JSON.stringify(savedPoints(saved)); const discard = () => !dirty || window.confirm('Du har ændringer, der ikke er gemt. Kassér ændringerne?'); const close = () => { if (!busy && discard()) onClose(); }; useEffect(() => { if (!dirty) return undefined; const warn = event => { event.preventDefault(); event.returnValue=''; }; window.addEventListener('beforeunload',warn); return () => window.removeEventListener('beforeunload',warn); },[dirty]); const save = async () => { setBusy(true);setError(''); try { const {geometry} = await request('PUT',{expectedRevision:revision,geometry:{type:'Polygon',coordinates:[[...points,points[0]]]}}); if (geometry?.schema !== 'site_geometry_v1' || !Number.isFinite(geometry?.totals?.groundAreaM2) || !geometry?.revision) throw new Error('Serveren returnerede ikke et gyldigt område.'); setSaved(geometry);setRevision(geometry.revision);onSave(geometry); } catch(e) { setError(e.message); } finally { setBusy(false); } }; const remove = async () => { setBusy(true);setError('');setStatus(''); try { const result = await request('DELETE',{expectedRevision:revision}); if (result?.geometry !== null || result?.deleted !== true || !Number.isSafeInteger(result?.revision) || result.revision <= revision) { throw new Error('Serveren bekræftede ikke sletningen med en ny revision.'); } setSaved(null);setPoints([]);setRevision(result.revision);setDrawing(true);setDeleteConfirm(false); setStatus('Kortområdet er slettet. Kortbaserede mængder kan ikke længere bruges.'); onDelete?.(result); } catch(e) { setError(e.message);setDeleteConfirm(false); } finally { setBusy(false); } }; const search = async event => { event.preventDefault();setBusy(true);setError(''); try { setCandidates((await request('POST',{query})).candidates); } catch(e) {setError(e.message);} finally {setBusy(false);} }; const updatePoint = (index,axis,value) => setPoints(current => current.map((p,i) => i===index ? p.map((v,a) => a===axis ? value : v) : p)); return <> Mål område på kort

Korttegnet · © OpenStreetMap-bidragsydere · ODbL · Ikke verificeret{saved?.audit?.updatedAt ? ` · Redigeret ${new Date(saved.audit.updatedAt).toLocaleDateString('da-DK')}` : ''}

{roof &&

Kortet måler kun grundprojektion. Skråt tagareal kræver et eksplicit taggrundlag med hældning eller en manuel tagopmåling.

}
{candidates.length > 0 && } {error && } {status &&

{status}

} {loading &&

Henter område…

} {loadFailed &&
}
{saved ? : }

Tryk på kortet for at tegne. Træk punkter for at redigere. Brug punktlisten til tastatur og præcise koordinater.

{roof ? 'Grundprojektion' : 'Grundareal'}: {preview(points).toLocaleString('da-DK',{maximumFractionDigits:1})} m² · Forhåndsvisning

Rediger punkter med tastatur {points.map((p,i) =>
Punkt {i+1} {[0,1].map(axis => )}
)}
{!loadFailed && }
!busy && setDeleteConfirm(false)} aria-labelledby="site-geometry-delete-title"> Slet gemt kortområde permanent?

Området fjernes med revisionsspor. Kortbaserede arbejdspakker og snapshots bliver ugyldige og skal opmåles igen eller erstattes med manuelle mål.

{dirty &&

Ikke-gemte ændringer i tegningen bliver også kasseret.

}
; }