Files
geointel/frontend/src/components/WorkbenchStatusStrip.tsx
T
Jens 91c1825892
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s
fix: restore GeoIntel and upgrade Atlas workbench
2026-07-26 00:45:44 +02:00

221 lines
7.7 KiB
TypeScript

import type { AreaRead, DatasetCreateResponse, ExportRead, ProjectRead, QualityCheckRead } from '../types'
import {
FLANDERS_WORKSPACE_LABEL,
FLANDERS_WORKSPACE_PROJECT_NAME,
REGIONAL_WORKSPACE_LABEL,
REGIONAL_WORKSPACE_PROJECT_NAME,
} from '../config/primaryFocus'
interface StatusItem {
key: string
label: string
value: string
detail: string
state: 'ready' | 'warning' | 'waiting'
}
interface WorkbenchStatusStripProps {
selectedProject: ProjectRead | null
areas: AreaRead[]
datasets: DatasetCreateResponse[]
qualityChecks: QualityCheckRead[]
exports: ExportRead[]
activeLayerFeatureCount: number
selectedAreaHasGeometry: boolean
}
function countByDatasetType(datasets: DatasetCreateResponse[], datasetType: string): number {
return datasets.filter((dataset) => dataset.dataset_type === datasetType).length
}
function countReferenceDatasets(datasets: DatasetCreateResponse[]): number {
return datasets.filter((dataset) => dataset.dataset_role === 'reference').length
}
const ANALYTICAL_SOURCE_NAMES = new Set([
'ngi_adminvector',
'rbins_marine_reporting_units',
'rbins_msp_2026',
'grb',
'department_omgeving_land_use',
'historical_landuse',
'department_omgeving_thematic_raster',
'statbel',
'inbo_bwk_natura2000',
'agentschap_landbouw_zeevisserij_agricultural_parcels',
'dov_soil_map',
'digitaal_vlaanderen_dhmv',
'spw_terrain',
'vmm_flood_hazard',
'vmm_vha_bathymetry_profiles',
])
const ANALYTICAL_LAYER_NAMES = new Set([
'administrative',
'belgium_land_boundary',
'belgium_regions',
'belgium_provinces',
'belgium_municipalities',
'buildings',
'roads',
'water',
'parcels',
'population',
'forest',
'nature_value',
'agriculture',
'soil',
'bathymetry',
'flood_hazard',
'elevation',
'maritime_planning',
'marine_environment',
'marine_legal_scopes',
'marine_spatial_plan_2026',
])
function isAnalyzableDataset(dataset: DatasetCreateResponse): boolean {
if (dataset.status !== 'ready') return false
const sourceName = String(dataset.source_name ?? dataset.source ?? '').toLowerCase()
const layerName = String(
dataset.source_metadata?.['theme']
?? dataset.reference_layer_name
?? dataset.source_metadata?.['layer_type']
?? '',
).toLowerCase()
return ANALYTICAL_SOURCE_NAMES.has(sourceName) || ANALYTICAL_LAYER_NAMES.has(layerName)
}
function statusLabel(state: StatusItem['state']): string {
if (state === 'ready') return 'gereed'
if (state === 'warning') return 'aandacht'
return 'nog open'
}
function projectLabel(project: ProjectRead | null): string {
if (!project) return 'Geen werkruimte'
if (project.name === FLANDERS_WORKSPACE_PROJECT_NAME) return FLANDERS_WORKSPACE_LABEL
return project.name === REGIONAL_WORKSPACE_PROJECT_NAME ? REGIONAL_WORKSPACE_LABEL : project.name
}
function nextAction(items: StatusItem[]): string {
if (!items.some((item) => item.key === 'project' && item.state === 'ready')) {
return 'De regionale werkruimte kon niet worden geopend.'
}
if (!items.some((item) => item.key === 'area' && item.state === 'ready')) {
return 'Kies een gemeente of de volledige regio.'
}
if (!items.some((item) => item.key === 'datasets' && item.state === 'ready')) {
return 'Laad een kaartlaag of luchtbeeld in.'
}
if (!items.some((item) => item.key === 'qa' && item.state === 'ready')) {
return 'Kaart en bronnen zijn klaar. Kwaliteitscontrole is beschikbaar zodra er een analyseresultaat is.'
}
if (!items.some((item) => item.key === 'exports' && item.state === 'ready')) {
return 'De analyse is klaar; maak eventueel een rapport of download.'
}
return 'Alle onderdelen van de werkruimte bevatten bewaarde resultaten.'
}
export function WorkbenchStatusStrip({
selectedProject,
areas,
datasets,
qualityChecks,
exports,
activeLayerFeatureCount,
selectedAreaHasGeometry,
}: WorkbenchStatusStripProps): JSX.Element {
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
const analyzableDatasets = datasets.filter(isAnalyzableDataset).length
const vectorDatasets = countByDatasetType(datasets, 'vector') + countByDatasetType(datasets, 'geojson')
const rasterDatasets = countByDatasetType(datasets, 'raster')
const referenceDatasets = countReferenceDatasets(datasets)
const latestQualityCheck = qualityChecks[0]
const latestExport = exports[0]
const items: StatusItem[] = [
{
key: 'project',
label: 'Werkruimte',
value: projectLabel(selectedProject),
detail: selectedProject ? selectedProject.region : 'Niet geladen',
state: selectedProject ? 'ready' : 'waiting',
},
{
key: 'area',
label: 'Gebieden',
value: `${areas.length} beschikbaar`,
detail: selectedAreaHasGeometry ? 'Kaartgrens beschikbaar' : 'Geen kaartgrens geselecteerd',
state: selectedAreaHasGeometry ? 'ready' : areas.length > 0 ? 'warning' : 'waiting',
},
{
key: 'datasets',
label: 'Bronnen',
value: `${analyzableDatasets} analysethema${analyzableDatasets === 1 ? '' : "'s"}`,
detail: (
`${readyDatasets}/${datasets.length} bronnen gereed · `
+ `${vectorDatasets} kaartlagen, ${rasterDatasets} rasters, ${referenceDatasets} officiële referenties`
),
state: analyzableDatasets === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning',
},
{
key: 'map',
label: 'Kaart',
value: `${activeLayerFeatureCount} zichtbare objecten`,
detail: selectedAreaHasGeometry ? 'Gebiedsgrens beschikbaar' : 'Gebiedsgrens ontbreekt',
state: selectedAreaHasGeometry || activeLayerFeatureCount > 0 ? 'ready' : 'waiting',
},
{
key: 'qa',
label: 'Kwaliteit',
value: `${qualityChecks.length} controles`,
detail: latestQualityCheck ? `${latestQualityCheck.check_type}: ${latestQualityCheck.status}` : 'Nog geen controle uitgevoerd',
state: qualityChecks.length > 0 ? 'ready' : 'waiting',
},
{
key: 'exports',
label: 'Downloads',
value: `${exports.length} bestanden`,
detail: latestExport ? `${latestExport.export_type}: ${latestExport.status}` : 'Nog geen bestand gemaakt',
state: exports.length > 0 ? 'ready' : 'waiting',
},
]
const readyCount = items.filter((item) => item.state === 'ready').length
const coreReady = items.slice(0, 4).every((item) => item.state === 'ready')
return (
<section className="workbench-status-strip" aria-label="Workbench readiness">
<div className="status-strip-header">
<div>
<p className="eyebrow">Platformstatus</p>
<h2>
<span className="sr-only">Status werkruimte: </span>
{coreReady ? 'Kaartwerkruimte is gebruiksklaar' : 'Kaartwerkruimte vraagt aandacht'}
</h2>
<p className="status-next-action">{nextAction(items)}</p>
</div>
<div className="status-readiness" data-ready={readyCount} aria-label={`${readyCount} van ${items.length} onderdelen gereed`}>
<span className="status-readiness-orbit" aria-hidden="true" />
<div className="status-readiness-copy">
<strong>{readyCount}/{items.length}</strong>
<span>onderdelen gereed</span>
</div>
</div>
</div>
<div className="status-strip-grid">
{items.map((item) => (
<div className={`status-tile status-tile-${item.state}`} key={item.key}>
<div className="status-tile-topline">
<span>{item.label}</span>
<span className="status-pill">{statusLabel(item.state)}</span>
</div>
<strong title={item.value}>{item.value}</strong>
<p>{item.detail}</p>
</div>
))}
</div>
</section>
)
}