Skip to content

Add Consultation#195

Open
kouloumos wants to merge 20 commits intoschemalabz:mainfrom
kouloumos:consultation-bins
Open

Add Consultation#195
kouloumos wants to merge 20 commits intoschemalabz:mainfrom
kouloumos:consultation-bins

Conversation

@kouloumos
Copy link
Member

@kouloumos kouloumos commented Feb 16, 2026

Note

Medium Risk
Touches the Nix-based preview/production startup wiring for .next artifacts; a mistake here could break preview deployments or cause stale/incorrectly regenerated pages. Other changes are documentation and test-runner tuning with low functional risk.

Overview
Adds internal documentation for the new public consultations feature (docs/guides/consultations.md) and clarifies that development commands should be run inside the Nix dev shell (CLAUDE.md).

Updates the Nix preview startup logic in flake.nix to ensure .next/server/app is copied into a writable work directory (instead of symlinked) so Next.js ISR can regenerate pre-rendered pages, with cleanup for upgrade paths.

Tunes Jest parallelism/memory (maxWorkers, workerIdleMemoryLimit) and adds a public/regulation-cooking-oil-geocode-failures.json artifact for geocoding failures.

Written by Cursor Bugbot for commit d8a8fb8. This will update automatically on new commits. Configure here.

Greptile Summary

This PR adds a comprehensive public consultations feature to OpenCouncil, allowing municipalities to run geospatial consultations with comment collection.

Key Changes:

  • New consultations feature with admin CRUD UI, API endpoints, and database layer
  • Geo utilities refactored from src/lib/utils.ts into dedicated src/lib/geo.ts with unit tests
  • Nix infrastructure updated to properly handle Next.js ISR by copying (not symlinking) .next/server/app directory for writability
  • Enhanced consultation map with search location mode, mobile drawer support, and nearby points detection
  • Email notifications for consultation comments sent to municipality with user CC
  • HTML sanitization for safe comment rendering
  • New internal documentation for the consultations feature

Quality Indicators:

  • Clean refactoring following DRY principles (geo utilities extracted to shared module)
  • Comprehensive test coverage for geo utilities
  • Proper authentication checks using withUserAuthorizedToEdit
  • Timezone-aware consultation end date handling
  • Input validation and error handling throughout
  • Previous review feedback addressed (removed as any casts, fixed ternary)

Confidence Score: 4/5

  • Safe to merge with careful monitoring of preview deployments
  • The code quality is high with proper testing, validation, and refactoring. The main risk is in the Nix infrastructure changes to flake.nix that modify how .next/server/app is handled in preview deployments - this is critical path code for ISR functionality. While the implementation looks correct, it touches deployment infrastructure that could break preview environments if something was missed.
  • Pay close attention to flake.nix lines 943-1324 (preview startup script) - ensure preview deployments work correctly with the new copy-instead-of-symlink approach for .next/server/app

Important Files Changed

Filename Overview
flake.nix Updates Nix preview startup to copy .next/server/app instead of symlinking for ISR compatibility, adds cleanup for upgrade paths
src/lib/geo.ts New file consolidating geo utilities (calculateGeometryBounds, createCircleBuffer, haversineDistance) extracted from utils.ts, well-tested
src/lib/tests/geo.test.ts Comprehensive unit tests for geo utilities covering edge cases and validation
src/lib/db/consultations.ts Core consultation data layer with CRUD operations, validation, timezone handling, and email notifications
src/app/api/admin/consultations/route.ts Admin API for creating and listing consultations with proper auth checks
src/lib/utils/sanitize.ts New HTML sanitization utility for consultation comments with safe tag allowlist
src/components/consultations/ConsultationMap.tsx Major enhancement adding search location mode, mobile drawer support, and extracted createCircleBuffer to shared lib
src/components/consultations/DetailPanel.tsx Added search-location detail mode with nearby points calculation and mobile drawer support

Last reviewed commit: 5722304

@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🚀 Preview deployment ready!

Preview URL: https://pr-195.preview.opencouncil.gr
Commit: 5722304
Database: Shared staging

The preview will be automatically updated when you push new commits.
It will be destroyed when this PR is closed or merged.


This preview uses the staging database - any changes will affect other previews.

@greptile-apps
Copy link

greptile-apps bot commented Feb 16, 2026

Greptile Summary

Large feature PR that adds a comprehensive admin CRUD interface for consultations, a community picker with address search for the consultation map view, a welcome dialog with regulation summary, zoom-to-geometry on click, initial fit-to-bounds, and supporting scripts/documentation for the Athens cooking oil regulation consultation.

  • Admin consultations page (/admin/consultations): New page for creating, editing, toggling, and deleting consultations with S3 file upload and inline URL editing. Auth is properly restricted to superadmins.
  • Community picker & address search: The LayerControlsPanel was refactored into a dual-mode component -- a simplified community picker for citizens (with address search via LocationSelector) and the full layer controls for admin editing mode.
  • Search location pins: Citizens can search addresses and see colored pins on the map with a detail panel showing nearby collection points within 500m (Haversine distance).
  • Welcome dialog: A Dialog component shows the regulation summary on first load with action buttons for finding your area, viewing the document, or browsing comments.
  • Map improvements: Click handler now prioritizes points over polygons, polygon/point labels at appropriate zoom levels, GeometryCollection support in bounds calculation, showStreetLabels prop, initial fit-to-bounds.
  • DRY cleanup: CurrentUser and GeoSetData interfaces consolidated into types.ts, eliminating 5+ duplicate definitions.
  • Relative URL support: fetchRegulationData now resolves relative URLs using env.NEXTAUTH_URL (though the function itself is still duplicated in 3 files).
  • Scripts: generate-cooking-oil-regulation.ts and geocode-regulation-addresses.ts for generating and geocoding regulation data from municipal PDFs.
  • Documentation: Comprehensive docs/guides/consultations.md covering architecture, component pointers, and business rules.

Confidence Score: 4/5

  • This PR is safe to merge with minor style issues to address.
  • The PR is well-structured with proper authentication on all admin endpoints, correct use of withUserAuthorizedToEdit({}) for superadmin-only access, and solid client-side state management. The issues found are primarily style-level (redundant ternary, as any cast, duplicated function, missing error handling in admin page). No security vulnerabilities or critical logic bugs were identified.
  • src/app/[locale]/(other)/admin/consultations/page.tsx has a redundant ternary and missing error handling. src/app/[locale]/(city)/[cityId]/consultation/[id]/page.tsx has an as any cast and duplicated fetchRegulationData.

Important Files Changed

Filename Overview
src/app/api/admin/consultations/route.ts New CRUD API for consultations. Auth properly uses withUserAuthorizedToEdit({}) (superadmin-only). Input validation present. No major issues.
src/app/api/admin/consultations/[id]/route.ts PUT/DELETE endpoints for consultations. Auth properly enforced. Prisma error handling is appropriate. No major issues.
src/app/[locale]/(other)/admin/consultations/page.tsx New admin page for managing consultations. Has a redundant ternary in the URL href and missing error handling in handleToggle/handleDelete.
src/components/consultations/ConsultationMap.tsx Significant enhancements: address search pins, initial fit-to-bounds, zoom-on-click, GeometryCollection support, search-location detail mode. Well-structured with refs to prevent state conflicts.
src/components/consultations/ConsultationViewer.tsx Added welcome dialog with regulation summary, defaultView support, city logo display. Hash-aware welcome dialog dismissal is a nice touch. Greek UI strings are hardcoded (expected for this project).
src/components/consultations/DetailPanel.tsx New search-location detail type with nearby points calculation using Haversine formula. Good use of useMemo for the distance computation. Reusable GeometryListItem component.
src/components/consultations/LayerControlsPanel.tsx Major refactor into dual-mode: normal mode (simplified community picker with address search) and editing mode (full layer controls). Clean separation of concerns.
src/components/map/map.tsx Enhanced map component: click handler now prioritizes points over polygons, new label layers for points and pinned labels, showStreetLabels prop, mapReady state. Removed redundant place/POI label layers.
src/components/consultations/types.ts Consolidated shared interfaces (CurrentUser, GeoSetData, SEARCH_COLORS) into a single types file, eliminating duplicated definitions across 5+ components. Good DRY improvement.
src/lib/db/consultations.ts Fixed relative URL resolution for regulation JSON by prepending env.NEXTAUTH_URL. Correctly uses dynamic import for env module.
src/lib/utils.ts Added GeometryCollection support to calculateGeometryBounds. Refactored to use processGeometry helper. Fixed Point bounds (now uses Math.min/max instead of direct assignment).
src/app/[locale]/(city)/[cityId]/consultation/[id]/page.tsx Passes cityName and cityLogoUrl to ConsultationViewer. Uses (city as any).logoImage cast which violates coding guidelines. Has duplicated fetchRegulationData function.

Sequence Diagram

sequenceDiagram
    participant Citizen as Citizen
    participant MapView as ConsultationMap
    participant Picker as LayerControlsPanel
    participant Detail as DetailPanel
    participant MapBox as Mapbox GL

    Note over Citizen, MapBox: Community Picker with Address Search
    Citizen->>Picker: Opens community picker
    Picker-->>Citizen: Shows list of communities

    Citizen->>Picker: Searches address via LocationSelector
    Picker->>MapView: onSearchLocation(location)
    MapView->>MapView: Add to searchLocations state
    MapView->>MapBox: Add colored pin feature
    MapView->>Detail: openSearchLocationDetail(location, index)
    Detail->>Detail: Compute nearbyPoints (Haversine, 500m radius)
    Detail-->>Citizen: Show nearest collection points sorted by distance

    Citizen->>Picker: Clicks community name
    Picker->>MapView: onOpenGeoSetDetail(geoSetId)
    MapView->>MapBox: Zoom to community points
    MapView->>Detail: Show geoset detail with point list

    Citizen->>MapBox: Clicks point on map
    MapBox->>MapView: handleMapFeatureClick (points prioritized)
    MapView->>Detail: openGeometryDetail(geometryId)
    MapView->>MapBox: Zoom to geometry
Loading

Last reviewed commit: 10bf549

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

27 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 16, 2026

Additional Comments (1)

src/app/[locale]/(city)/[cityId]/consultation/[id]/page.tsx
Duplicated fetchRegulationData function
This function is now duplicated in three places with identical logic (here, src/lib/db/consultations.ts:199, and src/app/[locale]/(city)/[cityId]/consultation/[id]/comments/page.tsx:16). Since this PR updated the URL resolution logic in all three copies, this is a good candidate for consolidation. Per the project's DRY guidelines, consider exporting the function from src/lib/db/consultations.ts and importing it in both page files.

Context Used: Context from dashboard - CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/(city)/[cityId]/consultation/[id]/page.tsx
Line: 14:30

Comment:
**Duplicated `fetchRegulationData` function**
This function is now duplicated in three places with identical logic (here, `src/lib/db/consultations.ts:199`, and `src/app/[locale]/(city)/[cityId]/consultation/[id]/comments/page.tsx:16`). Since this PR updated the URL resolution logic in all three copies, this is a good candidate for consolidation. Per the project's DRY guidelines, consider exporting the function from `src/lib/db/consultations.ts` and importing it in both page files.

**Context Used:** Context from `dashboard` - CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=a3ddaa95-717d-48e6-81cd-93c42696bbed))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@kouloumos
Copy link
Member Author

I force-pushed to address bot review comment(s) (1, 2, 3, 4):

  • Removed unnecessary as any casts for city.logoImage and city.consultationsEnabled (both fields exist on the Prisma City type)
  • Fixed redundant ternary c.jsonUrl.startsWith('http') ? c.jsonUrl : c.jsonUrlc.jsonUrl
  • Extracted haversineDistance to module scope (pure function with no component dependencies)
  • Consolidated duplicated fetchRegulationData into a single export from consultations.ts — also fixed the comments page copy which was missing relative URL resolution

@kouloumos
Copy link
Member Author

Duplicated fetchRegulationData function

Fixed in force-push — exported fetchRegulationData from consultations.ts and import it in both page files. This also fixed a bug in the comments page where the local copy was missing the relative URL resolution.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@kouloumos
Copy link
Member Author

kouloumos commented Feb 18, 2026

I force-pushed to address bot review comment(s) (1, 2, 3):

  • Add chmod -R u+w after copying .next/server/app from nix store so ISR can actually write
  • Remove stale app directory before copying to prevent nested app/app on redeploys
  • Remove old .next/server symlink from previous script version before mkdir -p (upgrade path)

Documents the consultation feature as actually implemented, covering
the regulation JSON schema, dual-view frontend, comment system,
geo-editor, and business rules.
Geocodes point geometries in regulation JSON files using Google
Geocoding API. Finds points with textualDefinition but no geojson
coordinates, geocodes each address scoped to Athens, and fills in
the geojson field. Supports --dry-run, --force, and --delay options.
Adds CRUD API routes and an admin UI for creating and managing
consultations. Includes city selector (filtered to consultationsEnabled
cities), regulation JSON URL input, end date picker, active toggle,
and a table listing all consultations with comment counts.
Server-side fetch cannot resolve relative paths like /regulation.json.
Prepend NEXTAUTH_URL to relative jsonUrl values in both the consultation
page and the comment validation helper.
Documents the regulation JSON production pipeline: PDF-to-JSON
conversion, coordinate transformation, address geocoding, and
consultation-specific generators. Includes a comparison table showing
which scripts were used for each consultation.
Reference clicks and comment navigation switch to map view but didn't
scroll to top, leaving the full-screen map hidden above the viewport.
When editing a point geometry, searching for an address now shows a
button to directly use that location as the point's coordinates.
Saves to localStorage like manual map clicks, with toast confirmation.
Clicking a point or polygon on the map now zooms to it. Hash-based
navigation (e.g. #dk_3) zooms to the geoset's boundary polygon.
Fixed zoom not firing when transitioning from document to map view
by tracking map initialization state so pending zooms execute once
the map is ready.
Admin dashboard now supports uploading regulation JSON files directly
to S3 and editing JSON URLs inline in the consultations table. Also
documents the regulation JSON hosting workflow.
…map view

Redesign the map sidebar into a dual-mode panel: a simplified community
picker (normal mode) with address search via LocationSelector, and the
full layer controls (editing mode) for admins. Citizens can search their
address to find nearby collection points within 500m, shown as colored
pins on the map. A welcome dialog presents the regulation summary on
first load with navigation options.

Also: extract shared types (CurrentUser, GeoSetData, SEARCH_COLORS) to
types.ts, deduplicate geometry list item rendering in DetailPanel, add
GeometryCollection support to bounds calculation, improve map labels
(point addresses at zoom, polygon names that fade), and update docs.
…in dev banner

Use hardcoded opencouncil.gr sender domain instead of deriving from
NEXTAUTH_URL, which breaks on preview deployments (unverified subdomain).
Add a dev banner to overridden emails showing original To and CC recipients.
Pre-rendered pages built during nix build contain stale data (e.g. "Test
City" from the build-time DB). The preview-start script symlinked the
entire .next/server directory from the read-only nix store, preventing
Next.js ISR from updating pages with fresh data at runtime (EROFS error).

Copy .next/server/app/ to a writable location instead of symlinking so
ISR can regenerate pre-rendered pages after the first real request.
Convert DetailPanel, LayerControlsPanel, and CommentsOverviewSheet to
use vaul Drawer on mobile (<768px) instead of side Sheet/Dialog. The
welcome dialog switches to Credenza for automatic desktop/mobile
adaptation.

- Non-modal drawers keep map interactive behind the sheet
- Only one bottom sheet open at a time (opening detail closes controls)
- Map zoom padding shifts content upward when drawer is open
- ViewToggleButton FAB repositions above any open drawer
- LayerControlsButton shows compact icon-only variant on mobile
- DrawerContent gains hideOverlay prop for non-modal usage
Deduplicate the identical HTML sanitization function that was defined
inline in both CommentSection and CommentsOverviewSheet.
Deduplicate the identical Haversine circle polygon generator that was
defined inline in both ConsultationMap and NotificationMapDialog.
The Sheet (desktop) branch duplicated ~150 lines of JSX that the
renderContent() helper already handles. Reuse renderContent() for both
the mobile Drawer and desktop Sheet containers.
…component

Extract raw Prisma queries from API routes into centralized functions
in src/lib/db/consultations.ts (getConsultationsForAdmin, createConsultation,
updateConsultation, deleteConsultation, getAdminCityOptions).

Split the admin consultations page into a server component (auth + initial
data fetch) and a client component (UI), matching the pattern used by
other admin pages like offers and meetings.
@kouloumos
Copy link
Member Author

Force-pushed with the following changes:

History rewrite — removed regulation .json data files from git history (2 JSON-only commits dropped, 2 others amended). The files will be deployed to S3 separately.

DRY refactors:

  • Extract getSafeHtmlContent to shared src/lib/utils/sanitize.ts (was duplicated in CommentSection + CommentsOverviewSheet)
  • Extract createCircleBuffer to shared geo utility (was duplicated in ConsultationMap + NotificationMapDialog)
  • Deduplicate CommentsOverviewSheet Drawer/Sheet branches — reuse renderContent() for both (-151 lines)
  • Move admin consultation Prisma queries to src/lib/db/consultations.ts and convert admin page to server component pattern (matching offers/meetings/etc.)
  • Consolidate all geo utilities (calculateGeometryBounds, createCircleBuffer, haversineDistance) into src/lib/geo.ts
  • Add unit tests for geo utilities (11 tests)

Move calculateGeometryBounds from lib/utils.ts, createCircleBuffer from
lib/geo/buffer.ts, and haversineDistance from DetailPanel.tsx into a
single shared geo module. Update all import sites.
Cover calculateGeometryBounds (Point, Polygon, MultiPolygon,
GeometryCollection, null, unsupported), createCircleBuffer (shape,
closure, radius accuracy), and haversineDistance (zero, known distance,
symmetry).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant