import { FormEvent, useMemo, useRef, useState } from 'react' import { PRIMARY_FOCUS_AREA_GEOJSON, PRIMARY_FOCUS_AREA_NAME, PRIMARY_FOCUS_REGION, NATIONAL_WORKSPACE_PROJECT_NAME, REGIONAL_WORKSPACE_PROJECT_NAME, isPrimaryFocusMunicipalityBoundaryDataset, isPrimaryFocusMunicipalityProject, isPrimaryFocusProject, isPrimaryFocusProjectData, } from '../config/primaryFocus' import { areasApi } from '../services/api/areas' import { datasetsApi } from '../services/api/datasets' import { projectsApi } from '../services/api/projects' import type { AreaCreate, AreaListResponse, AreaRead, DatasetCreateResponse, DatasetListResponse, ProjectCreate, ProjectRead, } from '../types' const DEMO_PROJECT_NAME = 'GeoIntel Demo - Building QA' function isDemoProject(project: ProjectRead): boolean { return project.name === DEMO_PROJECT_NAME } function hasMappedAnalysisContext( data: { areas: AreaRead[]; datasets: DatasetCreateResponse[] }, ): boolean { const hasRasterDataset = data.datasets.some((dataset) => dataset.dataset_type === 'raster') const hasVectorDataset = data.datasets.some( (dataset) => dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson', ) return data.areas.length > 0 && hasRasterDataset && hasVectorDataset } export function useProjectWorkspace() { const [projects, setProjects] = useState([]) const [selectedProjectId, setSelectedProjectId] = useState(null) const [areas, setAreas] = useState([]) const [datasets, setDatasets] = useState([]) const [loadingProjects, setLoadingProjects] = useState(false) const [loadingAreas, setLoadingAreas] = useState(false) const [loadingDatasets, setLoadingDatasets] = useState(false) const [archivingProjectId, setArchivingProjectId] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const projectDataRequestSequence = useRef(0) const [projectForm, setProjectForm] = useState({ name: '', description: '', region: PRIMARY_FOCUS_REGION, }) const [areaForm, setAreaForm] = useState({ name: PRIMARY_FOCUS_AREA_NAME, geometry: PRIMARY_FOCUS_AREA_GEOJSON, crs: 'EPSG:4326', }) const selectedProject = useMemo( () => projects.find((project) => project.id === selectedProjectId) ?? null, [projects, selectedProjectId], ) const fetchProjectData = async (projectId: string): Promise<{ areas: AreaRead[]; datasets: DatasetCreateResponse[] }> => { const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([ areasApi.list(projectId), datasetsApi.list(projectId), ]) return { areas: areaResponse.items, datasets: datasetResponse.items } } const pickInitialProjectId = async (items: ProjectRead[], preferredProjectId?: string | null) => { if (preferredProjectId && items.some((project) => project.id === preferredProjectId)) { return preferredProjectId } if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) { return selectedProjectId } const nationalProject = items.find((project) => project.name === NATIONAL_WORKSPACE_PROJECT_NAME) if (nationalProject) { try { const data = await fetchProjectData(nationalProject.id) if (data.areas.length > 0 && data.datasets.some((dataset) => dataset.status === 'ready')) { return nationalProject.id } } catch { // Continue with regional and municipality fallbacks while national data is unavailable. } } const regionalProject = items.find((project) => project.name === REGIONAL_WORKSPACE_PROJECT_NAME) if (regionalProject) { try { const data = await fetchProjectData(regionalProject.id) if (data.areas.length > 0 && data.datasets.some((dataset) => dataset.status === 'ready')) { return regionalProject.id } } catch { // Continue with the municipality and completeness fallbacks when regional data is unavailable. } } const municipalityProject = items.find(isPrimaryFocusMunicipalityProject) if (municipalityProject) { try { const data = await fetchProjectData(municipalityProject.id) if (data.areas.length > 0 && data.datasets.some(isPrimaryFocusMunicipalityBoundaryDataset)) { return municipalityProject.id } } catch { // Continue with the normal completeness ranking if the canonical workspace is temporarily unavailable. } } const primaryProjects = items.filter(isPrimaryFocusProject) const demoProjects = items.filter(isDemoProject) const candidates = Array.from( new Map( [...primaryProjects, ...demoProjects, ...items].map((project) => [project.id, project]), ).values(), ).slice(0, 12) const inspectedCandidates: Array<{ project: ProjectRead data: { areas: AreaRead[]; datasets: DatasetCreateResponse[] } }> = [] for (const project of candidates) { try { const data = await fetchProjectData(project.id) inspectedCandidates.push({ project, data }) if ( isPrimaryFocusProjectData(project, data.datasets) && hasMappedAnalysisContext(data) ) { return project.id } } catch { // Project list should still render if a candidate's detail endpoints are temporarily unavailable. } } const primaryContext = inspectedCandidates.find( ({ project, data }) => data.datasets.length > 0 && isPrimaryFocusProjectData(project, data.datasets), ) if (primaryContext) { return primaryContext.project.id } const completeContext = inspectedCandidates.find( ({ data }) => data.areas.length > 0 && data.datasets.length > 0, ) return completeContext?.project.id ?? primaryProjects[0]?.id ?? demoProjects[0]?.id ?? items[0]?.id ?? null } const loadProjects = async (preferredProjectId?: string | null) => { setLoadingProjects(true) setErrorMessage(null) try { const [response, nationalResponse, canonicalResponse] = await Promise.all([ projectsApi.list(), projectsApi.list({ name: NATIONAL_WORKSPACE_PROJECT_NAME, limit: 1 }), projectsApi.list({ name: REGIONAL_WORKSPACE_PROJECT_NAME, limit: 1 }), ]) const regionalItems = [...canonicalResponse.items, ...response.items] const items = Array.from( new Map( [...nationalResponse.items, ...regionalItems].map((project) => [project.id, project]), ).values(), ) setProjects(items) const nextProjectId = await pickInitialProjectId(items, preferredProjectId) if (nextProjectId && nextProjectId !== selectedProjectId) { setSelectedProjectId(nextProjectId) } } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Werkruimtes konden niet worden geladen') } finally { setLoadingProjects(false) } } const loadProjectData = async (projectId: string) => { const requestId = ++projectDataRequestSequence.current setLoadingAreas(true) setLoadingDatasets(true) setErrorMessage(null) try { const projectData = await fetchProjectData(projectId) if (requestId !== projectDataRequestSequence.current) { return null } setAreas(projectData.areas) setDatasets(projectData.datasets) return projectData } catch (error) { if (requestId === projectDataRequestSequence.current) { setErrorMessage(error instanceof Error ? error.message : 'De gegevens van de werkruimte konden niet worden geladen') } return null } finally { if (requestId === projectDataRequestSequence.current) { setLoadingAreas(false) setLoadingDatasets(false) } } } const createProject = async (event: FormEvent) => { event.preventDefault() if (!projectForm.name.trim()) { setErrorMessage('Geef een naam voor de werkruimte op') return } try { const createdProject = await projectsApi.create({ name: projectForm.name.trim(), description: projectForm.description?.trim() || undefined, region: projectForm.region?.trim() || PRIMARY_FOCUS_REGION, }) setProjectForm((previous) => ({ ...previous, name: '', description: '' })) setSelectedProjectId(createdProject.id) await loadProjects(createdProject.id) } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'De werkruimte kon niet worden aangemaakt') } } const createArea = async (event: FormEvent) => { event.preventDefault() if (!selectedProjectId) { setErrorMessage('Kies eerst een werkruimte') return } let geometry: AreaCreate['geometry'] try { geometry = JSON.parse(areaForm.geometry) as AreaCreate['geometry'] } catch { setErrorMessage('De opgegeven GeoJSON-geometrie is ongeldig') return } try { await areasApi.create(selectedProjectId, { name: areaForm.name, crs: areaForm.crs, geometry, }) await loadProjectData(selectedProjectId) setAreaForm((previous) => ({ ...previous, name: '' })) } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Het gebied kon niet worden aangemaakt') } } const archiveProject = async (projectId: string) => { setArchivingProjectId(projectId) setErrorMessage(null) try { await projectsApi.update(projectId, { status: 'archived' }) if (selectedProjectId === projectId) { setSelectedProjectId(null) resetProjectData() } await loadProjects() } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'De werkruimte kon niet worden gearchiveerd') } finally { setArchivingProjectId(null) } } const resetProjectData = () => { projectDataRequestSequence.current += 1 setAreas([]) setDatasets([]) } return { projects, selectedProject, selectedProjectId, areas, datasets, loadingProjects, loadingAreas, loadingDatasets, archivingProjectId, errorMessage, projectForm, areaForm, loadProjects, loadProjectData, createProject, createArea, archiveProject, resetProjectData, setSelectedProjectId, setErrorMessage, setProjectForm, setAreaForm, } }