@@ -257,8 +293,42 @@ export function DatasetPanel({
) : null}
Beschikbare bronnen
+
+
+
+ {filteredPrimaryDatasets.length} van {primaryDatasets.length}
+
+ {page} / {pageCount}
+
+
+
- {primaryDatasets.map((dataset) => {
+ {visiblePrimaryDatasets.map((dataset) => {
const datasetRole = normalizeDatasetRole(dataset)
const roleLabel = datasetRoleLabel(datasetRole)
return (
@@ -337,13 +407,19 @@ export function DatasetPanel({
)
})}
+ {visiblePrimaryDatasets.length === 0 && datasets.length > 0 ? (
+
Geen bron gevonden voor deze zoekterm.
+ ) : null}
{historicalDatasets.length > 0 ? (
-
+ setHistoryOpen(event.currentTarget.open)}
+ >
Historische meetmomenten
{historicalDatasets.length} oudere lagen
- : null}
) : null}
diff --git a/frontend/src/components/project/AreaPanel.tsx b/frontend/src/components/project/AreaPanel.tsx
index 04aec770..9af0d6a6 100644
--- a/frontend/src/components/project/AreaPanel.tsx
+++ b/frontend/src/components/project/AreaPanel.tsx
@@ -1,6 +1,8 @@
-import type { FormEvent } from 'react'
+import { useEffect, useMemo, useState, type FormEvent } from 'react'
import type { AreaRead, ProjectRead } from '../../types'
+const AREA_CATALOG_PAGE_SIZE = 12
+
function projectDisplayName(project: ProjectRead | null): string {
if (project?.name === 'Kempen Regional Workbench') {
return 'Kempen ยท volledige regio'
@@ -40,7 +42,30 @@ export function AreaPanel({
onUpdateAreaForm,
onSelectMapArea,
}: AreaPanelProps): JSX.Element {
+ const [catalogOpen, setCatalogOpen] = useState(false)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [page, setPage] = useState(1)
const selectedArea = areas.find((area) => area.id === selectedMapAreaId)
+ const filteredAreas = useMemo(() => {
+ const query = searchQuery.trim().toLocaleLowerCase('nl-BE')
+ if (!query) return areas
+ return areas.filter((area) => area.name.toLocaleLowerCase('nl-BE').includes(query))
+ }, [areas, searchQuery])
+ const pageCount = Math.max(1, Math.ceil(filteredAreas.length / AREA_CATALOG_PAGE_SIZE))
+ const visibleAreas = filteredAreas.slice(
+ (page - 1) * AREA_CATALOG_PAGE_SIZE,
+ page * AREA_CATALOG_PAGE_SIZE,
+ )
+
+ useEffect(() => {
+ setPage((current) => Math.min(current, pageCount))
+ }, [pageCount])
+
+ useEffect(() => {
+ setSearchQuery('')
+ setPage(1)
+ setCatalogOpen(false)
+ }, [selectedProjectId])
return (
@@ -94,28 +119,73 @@ export function AreaPanel({
{loadingAreas ? Gebieden laden...
: null}
{areas.length === 0 ? Nog geen gebieden beschikbaar.
: null}
-
+ setCatalogOpen(event.currentTarget.open)}
+ >
{areas.length} beschikbare gemeenten en regiogrenzen
-
- {areas.map((area) => (
- -
- {area.name}
-
-
{area.area_m2 ? `${(area.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Oppervlakte onbekend'}
-
{area.geometry?.type ?? area.geometry_type ?? 'Geometrie onbekend'}
+ {catalogOpen ? (
+ <>
+
+
+
+ {filteredAreas.length} gevonden
+
+ {page} / {pageCount}
+
-
-
- ))}
-
+
+ {visibleAreas.length > 0 ? (
+
+ ) : (
+
Geen gebied gevonden voor deze zoekterm.
+ )}
+ >
+ ) : null}
)
diff --git a/frontend/src/services/api/areas.ts b/frontend/src/services/api/areas.ts
index cafdec79..853621a9 100644
--- a/frontend/src/services/api/areas.ts
+++ b/frontend/src/services/api/areas.ts
@@ -1,8 +1,35 @@
import { apiGet, apiPost, apiPatch } from './client'
import type { AreaCreate, AreaListResponse, AreaRead } from '../../types'
+const AREA_PAGE_SIZE = 200
+
+async function listProjectAreas(projectId: string): Promise
{
+ const items: AreaRead[] = []
+ let offset = 0
+ let total: number | null = null
+ do {
+ const page = await apiGet(
+ `/api/v1/projects/${projectId}/areas?limit=${AREA_PAGE_SIZE}&offset=${offset}`,
+ )
+ if (total === null) {
+ total = page.total
+ } else if (page.total !== total) {
+ throw new Error('De gebiedslijst wijzigde tijdens het laden. Vernieuw de werkruimte.')
+ }
+ items.push(...page.items)
+ if (page.items.length === 0) {
+ break
+ }
+ offset += page.items.length
+ } while (offset < (total ?? 0))
+ if (total !== null && items.length !== total) {
+ throw new Error(`Niet alle gebieden konden worden geladen (${items.length}/${total}).`)
+ }
+ return { items, total: total ?? 0, limit: items.length, offset: 0 }
+}
+
export const areasApi = {
- list: (projectId: string): Promise => apiGet(`/api/v1/projects/${projectId}/areas`),
+ list: listProjectAreas,
create: (projectId: string, payload: AreaCreate): Promise =>
apiPost(`/api/v1/projects/${projectId}/areas`, payload),
get: (projectId: string, areaId: string): Promise =>
diff --git a/frontend/src/styles/premium.css b/frontend/src/styles/premium.css
index 51ea62ba..a19c1e49 100644
--- a/frontend/src/styles/premium.css
+++ b/frontend/src/styles/premium.css
@@ -571,6 +571,54 @@ details.data-panel-form-block > form {
background: transparent;
}
+.catalog-browser-toolbar {
+ display: grid;
+ gap: 0.55rem;
+ padding: 0.65rem;
+ border-bottom: 1px solid var(--line);
+ background: #f8fafb;
+}
+
+.catalog-browser-toolbar-datasets {
+ margin-bottom: 0.65rem;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+}
+
+.catalog-search-field {
+ display: grid;
+ gap: 0.3rem;
+ color: #53616d;
+ font-size: 0.74rem;
+ font-weight: 750;
+}
+
+.catalog-search-field input {
+ margin: 0;
+ background: #ffffff;
+}
+
+.catalog-pagination {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+ align-items: center;
+ color: var(--muted);
+ font-size: 0.76rem;
+}
+
+.catalog-pagination strong {
+ min-width: 3.5rem;
+ color: var(--ink);
+ text-align: center;
+}
+
+.catalog-pagination .secondary-action {
+ width: auto;
+ min-height: 2rem;
+ padding: 0.35rem 0.6rem;
+}
+
/* AI and review workspaces */
.ai-lab-shell {