Connect live analysis journey to map workflow
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LiveAnalysisJourney, resolveLiveAnalysisJourney } from './LiveAnalysisJourney'
|
||||
|
||||
const baseState = {
|
||||
selectionMode: false,
|
||||
hasSelection: false,
|
||||
sourceLoading: false,
|
||||
sourceReady: false,
|
||||
processing: false,
|
||||
validating: false,
|
||||
hasResult: false,
|
||||
verified: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
describe('resolveLiveAnalysisJourney', () => {
|
||||
it('follows real workflow state without assuming completion', () => {
|
||||
expect(resolveLiveAnalysisJourney(baseState).currentLabel).toBe('Selecteer')
|
||||
expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceLoading: true }).currentLabel).toBe('Bronnen')
|
||||
expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, processing: true }).currentLabel).toBe('Verwerk')
|
||||
expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, hasResult: true }).currentLabel).toBe('Controleer')
|
||||
expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, hasResult: true, verified: true }).status).toBe('complete')
|
||||
})
|
||||
|
||||
it('marks the reached stage as failed', () => {
|
||||
const view = resolveLiveAnalysisJourney({
|
||||
...baseState,
|
||||
hasSelection: true,
|
||||
sourceReady: true,
|
||||
processing: true,
|
||||
error: 'Bron tijdelijk niet beschikbaar.',
|
||||
})
|
||||
expect(view.status).toBe('error')
|
||||
expect(view.steps[2]).toBe('error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('LiveAnalysisJourney', () => {
|
||||
it('announces verified evidence only when verification exists', () => {
|
||||
render(
|
||||
<LiveAnalysisJourney
|
||||
{...baseState}
|
||||
hasSelection
|
||||
sourceReady
|
||||
hasResult
|
||||
verified
|
||||
selectionLabel="Vrije kaartselectie"
|
||||
sourceLabel="Gebouwen"
|
||||
statusMessage="Analyse voltooid"
|
||||
resultLabel="Kwaliteitsbewijs beschikbaar"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getAllByText('Kwaliteitsbewijs beschikbaar')).toHaveLength(2)
|
||||
expect(screen.getByTestId('live-analysis-journey').getAttribute('data-stage')).toBe('controleer')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { BadgeCheck, Database, MapPinned, ScanSearch, ShieldAlert } from 'lucide-react'
|
||||
|
||||
export type LiveAnalysisStepStatus = 'waiting' | 'active' | 'complete' | 'error'
|
||||
|
||||
export interface LiveAnalysisJourneyState {
|
||||
selectionMode: boolean
|
||||
hasSelection: boolean
|
||||
sourceLoading: boolean
|
||||
sourceReady: boolean
|
||||
processing: boolean
|
||||
validating: boolean
|
||||
hasResult: boolean
|
||||
verified: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface LiveAnalysisJourneyView {
|
||||
currentIndex: number
|
||||
currentLabel: string
|
||||
status: 'idle' | 'running' | 'complete' | 'error'
|
||||
steps: LiveAnalysisStepStatus[]
|
||||
}
|
||||
|
||||
const STEP_LABELS = ['Selecteer', 'Bronnen', 'Verwerk', 'Controleer'] as const
|
||||
|
||||
export function resolveLiveAnalysisJourney(state: LiveAnalysisJourneyState): LiveAnalysisJourneyView {
|
||||
let currentIndex = 0
|
||||
if (state.validating || state.verified || state.hasResult) currentIndex = 3
|
||||
else if (state.processing) currentIndex = 2
|
||||
else if (state.hasSelection) currentIndex = state.sourceLoading || !state.sourceReady ? 1 : 2
|
||||
|
||||
const status: LiveAnalysisJourneyView['status'] = state.error
|
||||
? 'error'
|
||||
: state.verified
|
||||
? 'complete'
|
||||
: state.sourceLoading || state.processing || state.validating
|
||||
? 'running'
|
||||
: 'idle'
|
||||
|
||||
const steps: LiveAnalysisStepStatus[] = [
|
||||
state.hasSelection ? 'complete' : 'active',
|
||||
state.hasSelection && state.sourceReady ? 'complete' : state.hasSelection ? 'active' : 'waiting',
|
||||
state.hasResult ? 'complete' : state.processing || (state.hasSelection && state.sourceReady) ? 'active' : 'waiting',
|
||||
state.verified ? 'complete' : state.validating || state.hasResult ? 'active' : 'waiting',
|
||||
]
|
||||
|
||||
if (state.error) steps[currentIndex] = 'error'
|
||||
|
||||
return { currentIndex, currentLabel: STEP_LABELS[currentIndex], status, steps }
|
||||
}
|
||||
|
||||
interface LiveAnalysisJourneyProps extends LiveAnalysisJourneyState {
|
||||
selectionLabel: string
|
||||
sourceLabel: string
|
||||
statusMessage: string
|
||||
resultLabel: string
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{ label: 'Selecteer', icon: MapPinned },
|
||||
{ label: 'Bronnen', icon: Database },
|
||||
{ label: 'Verwerk', icon: ScanSearch },
|
||||
{ label: 'Controleer', icon: BadgeCheck },
|
||||
] as const
|
||||
|
||||
export function LiveAnalysisJourney({ selectionLabel, sourceLabel, statusMessage, resultLabel, ...state }: LiveAnalysisJourneyProps): JSX.Element {
|
||||
const view = resolveLiveAnalysisJourney(state)
|
||||
const detail = view.currentIndex === 0
|
||||
? state.selectionMode ? 'Teken nu op de kaart' : selectionLabel
|
||||
: view.currentIndex === 1
|
||||
? sourceLabel
|
||||
: view.currentIndex === 3 && state.verified
|
||||
? resultLabel
|
||||
: statusMessage
|
||||
|
||||
return (
|
||||
<div className={`live-analysis-journey live-analysis-journey-${view.status}`} data-testid="live-analysis-journey" data-stage={view.currentLabel.toLocaleLowerCase('nl-BE')} aria-live="polite">
|
||||
<div className="live-analysis-status-card">
|
||||
<span className="live-analysis-kicker"><i aria-hidden="true" /> Live analyse</span>
|
||||
<strong>{state.error ? 'Analyse onderbroken' : view.currentLabel}</strong>
|
||||
<small>{state.error ?? detail}</small>
|
||||
</div>
|
||||
<ol className="live-analysis-steps" aria-label="Voortgang van de gebiedsanalyse">
|
||||
{steps.map(({ label, icon: Icon }, index) => (
|
||||
<li className={`live-analysis-step live-analysis-step-${view.steps[index]}`} key={label}>
|
||||
<span><Icon aria-hidden="true" /></span>
|
||||
<small>{label}</small>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className={`live-analysis-proof live-analysis-proof-${state.verified ? 'ready' : 'pending'}`}>
|
||||
{state.error ? <ShieldAlert aria-hidden="true" /> : <BadgeCheck aria-hidden="true" />}
|
||||
<span>{state.verified ? resultLabel : state.hasResult ? 'Resultaat wacht op controle' : 'Controleerbaar resultaat'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { useTemporalComparison } from '../../hooks/useTemporalComparison'
|
||||
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
|
||||
import { TemporalTrendChart } from './TemporalTrendChart'
|
||||
import { MunicipalitySearch } from './MunicipalitySearch'
|
||||
import { LiveAnalysisJourney } from './LiveAnalysisJourney'
|
||||
import { terrainImageUrl } from '../../lib/terrainImage'
|
||||
import { floodHazardImageUrl } from '../../lib/floodHazardImage'
|
||||
import { thematicRasterImageUrl, walousRasterImageUrl } from '../../lib/thematicRaster'
|
||||
@@ -1310,6 +1311,39 @@ export function MapWorkspace({
|
||||
: onDemandProductMap.get(activeTheme.id) ?? null
|
||||
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
|
||||
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
|
||||
const liveJourneyError = mapSelectionError
|
||||
?? themeResultsError
|
||||
?? temporalComparisonError
|
||||
?? orthophotoAnalysisError
|
||||
?? coverageError
|
||||
?? workspaceError
|
||||
const liveJourneyHasResult = Boolean(
|
||||
mapSelectionResult
|
||||
|| themeInsights.length > 0
|
||||
|| temporalComparison
|
||||
|| orthophotoResult
|
||||
|| analysisOverlayActive,
|
||||
)
|
||||
const liveJourneyVerified = Boolean(mapSelectionQaResult || orthophotoAnalysisQuality)
|
||||
const liveJourneyProcessing = Boolean(
|
||||
mapSelectionLoading
|
||||
|| themeResultsLoading
|
||||
|| temporalComparisonLoading
|
||||
|| orthophotoAnalysisRunning,
|
||||
)
|
||||
const liveJourneyValidating = Boolean(
|
||||
mapSelectionQaRunning || orthophotoAnalysisStage === 'validating',
|
||||
)
|
||||
const liveJourneyStatus = orthophotoAnalysisStatus
|
||||
|| (temporalComparisonLoading ? 'Officiële meetmomenten vergelijken…' : '')
|
||||
|| (themeResultsLoading ? 'Begrensde bronnen verwerken…' : '')
|
||||
|| (mapSelectionLoading ? 'Objecten binnen de selectie ophalen…' : '')
|
||||
|| (liveJourneyHasResult ? 'Resultaat op de kaart beschikbaar' : 'Klaar om de selectie te verwerken')
|
||||
const liveJourneyResultLabel = liveJourneyVerified
|
||||
? 'Kwaliteitsbewijs beschikbaar'
|
||||
: latestMapSelectionQualityCheckId
|
||||
? 'Bewaard kwaliteitsbewijs beschikbaar'
|
||||
: 'Controleerbaar resultaat'
|
||||
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
|
||||
const orthophotoImageOverlay = useMemo(
|
||||
() => orthophotoResult && orthophotoImageUrl && orthophotoResult.bbox_epsg4326.length === 4
|
||||
@@ -2609,6 +2643,21 @@ export function MapWorkspace({
|
||||
onMapBboxSelect={handleMapBboxSelect}
|
||||
onViewportChange={onMapViewportChange}
|
||||
/>
|
||||
<LiveAnalysisJourney
|
||||
selectionMode={bboxSelectionMode}
|
||||
hasSelection={Boolean(mapSelectionBbox)}
|
||||
sourceLoading={workspaceLoading || coverageLoading || officialMapProductsLoading}
|
||||
sourceReady={activeThemeAvailable && !workspaceLoading && !coverageLoading}
|
||||
processing={liveJourneyProcessing}
|
||||
validating={liveJourneyValidating}
|
||||
hasResult={liveJourneyHasResult}
|
||||
verified={liveJourneyVerified}
|
||||
error={liveJourneyError}
|
||||
selectionLabel={mapSelectionBbox ? 'Begrensde kaartselectie' : selectedMapArea?.name ?? 'Nog geen gebied geselecteerd'}
|
||||
sourceLabel={activeThemeAvailable ? activeTheme.shortLabel : 'Dekking wordt gecontroleerd'}
|
||||
statusMessage={liveJourneyStatus}
|
||||
resultLabel={liveJourneyResultLabel}
|
||||
/>
|
||||
<div className="geo-map-legend" aria-label="Kaartlegende">
|
||||
<span><i className="geo-legend-area" /> Werkgebied</span>
|
||||
{thematicRasterImageOverlays.length > 0 || walousRasterImageOverlays.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user