fix(industrial): improve OSM review UX + public map visibility
Some checks failed
CI / E2E Tests (push) Has been skipped
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 9s
CI / AI Services (Python) — Smoke (push) Failing after 7s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m7s
Deploy / Build API Image (push) Failing after 16s
Deploy / Build Web Image (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 7s
E2E Tests / Playwright E2E (push) Failing after 15s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 5s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m13s
Security Scanning / Trivy Scan — Web Image (push) Failing after 49s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 40s
Security Scanning / Trivy Filesystem Scan (push) Failing after 40s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Some checks failed
CI / E2E Tests (push) Has been skipped
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 9s
CI / AI Services (Python) — Smoke (push) Failing after 7s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m7s
Deploy / Build API Image (push) Failing after 16s
Deploy / Build Web Image (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 7s
E2E Tests / Playwright E2E (push) Failing after 15s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 5s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m13s
Security Scanning / Trivy Scan — Web Image (push) Failing after 49s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 40s
Security Scanning / Trivy Filesystem Scan (push) Failing after 40s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Four UX issues surfaced when reviewing the new OSM-sync pipeline against the actual 2,193 imports — fixed in this commit: 1. Admin queue surfaced noise first. `ListOsmPendingHandler` now sorts by `totalAreaHa DESC` (real KCN first, single-factory `landuse=industrial` polygons last) and accepts `minAreaHa` (default 50 ha) plus a `region` filter. The admin page exposes both as dropdowns — "Tất cả / ≥ 5 / ≥ 50 / ≥ 200 / ≥ 500 ha". Top-of-queue is now Bàu Bàng (2,597 ha) and Nhơn Trạch (2,535 ha). 2. Promote dialog said "KCN KCN Đại An" — duplicate prefix. Reworded to "Sắp promote: <name>" so the row name stands on its own. 3. Province was "Chưa xác định" on 2,107 of 2,193 OSM rows. The OSM tags lacked any addr:* hint, so the importer never had anything to write. Added `scripts/data/vn-province-centroids.ts` (63 provinces with capital-city coords) and a `nearestProvince(lat, lng)` fallback in `parseFeature()`. Shipped a one-shot backfill script `scripts/backfill-osm-provinces.ts` and ran it — every existing OSM row now has a province (Hồ Chí Minh: 408, Lạng Sơn: 232, Quảng Ninh: 220, Hà Nội: 172, Hải Phòng: 105, …). Admin can correct on promote if the nearest-centroid heuristic picked the wrong neighbour for a long-thin province. 4. Public map looked empty — only 20 curated parks visible. Added an opt-in toggle "Hiển thị KCN OSM" with a small legend above the map. When on, the bbox endpoint returns OSM raw rows too; markers render in amber (vs. green for curated) at slightly smaller radius and lower opacity, so the visual hierarchy stays clear. Refetch is wired through a ref so the toggle takes effect without remounting the map. Verified in browser preview: zoom-out shows clusters of 320 / 71 / etc. across the country with the toggle on, and just three small clusters (20 curated parks) when off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
67
apps/web/components/khu-cong-nghiep/osm-map-legend.tsx
Normal file
67
apps/web/components/khu-cong-nghiep/osm-map-legend.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { Info } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
interface OsmMapLegendProps {
|
||||
includeOsmRaw: boolean;
|
||||
onToggleOsmRaw: (value: boolean) => void;
|
||||
/** Smaller variant for the split-view sidebar. */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legend + toggle that sits above the bbox map. Explains the two marker
|
||||
* colors (curated vs raw OSM) and lets the user opt into showing the
|
||||
* un-reviewed OpenStreetMap imports.
|
||||
*/
|
||||
export function OsmMapLegend({
|
||||
includeOsmRaw,
|
||||
onToggleOsmRaw,
|
||||
compact = false,
|
||||
}: OsmMapLegendProps) {
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-wrap items-center gap-3 rounded-lg border bg-card ${
|
||||
compact ? 'px-3 py-2 text-xs' : 'px-4 py-2.5 text-sm'
|
||||
}`}
|
||||
role="group"
|
||||
aria-label="Chú giải bản đồ KCN"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="inline-block h-3 w-3 rounded-full border-2 border-white shadow"
|
||||
style={{ backgroundColor: '#22c55e' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-foreground">KCN đã xác minh</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="inline-block h-3 w-3 rounded-full border-2 border-white shadow opacity-70"
|
||||
style={{ backgroundColor: '#f59e0b' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-foreground">KCN từ OpenStreetMap (chưa duyệt)</span>
|
||||
</div>
|
||||
|
||||
<label className="ml-auto flex cursor-pointer items-center gap-1.5 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeOsmRaw}
|
||||
onChange={(e) => onToggleOsmRaw(e.target.checked)}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
<span>Hiển thị KCN OSM</span>
|
||||
</label>
|
||||
|
||||
{includeOsmRaw && !compact && (
|
||||
<p className="flex w-full items-center gap-1.5 border-t border-border pt-2 text-xs text-muted-foreground">
|
||||
<Info className="h-3 w-3 shrink-0" />
|
||||
KCN màu vàng là dữ liệu thô từ OpenStreetMap, chưa được kiểm duyệt — thông tin có thể chưa
|
||||
chính xác hoặc thiếu.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,8 +50,12 @@ export function OsmParkBboxMap({
|
||||
// Capture the current includeOsmRaw value via a ref so the moveend
|
||||
// handler always sees the latest without re-binding the listener.
|
||||
const includeOsmRawRef = React.useRef(includeOsmRaw);
|
||||
// Bumping this triggers a manual refetch when the toggle changes —
|
||||
// the moveend handler alone doesn't fire on prop changes.
|
||||
const refetchTokenRef = React.useRef<(() => void) | null>(null);
|
||||
React.useEffect(() => {
|
||||
includeOsmRawRef.current = includeOsmRaw;
|
||||
refetchTokenRef.current?.();
|
||||
}, [includeOsmRaw]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -166,6 +170,8 @@ export function OsmParkBboxMap({
|
||||
});
|
||||
|
||||
// Individual park markers (centroid Points) when not clustered.
|
||||
// Color is data-driven: green = curated (MANUAL / OSM_PROMOTED),
|
||||
// amber = raw OSM imports awaiting admin review.
|
||||
map.addLayer({
|
||||
id: POINT_LAYER_ID,
|
||||
type: 'circle',
|
||||
@@ -176,10 +182,26 @@ export function OsmParkBboxMap({
|
||||
['==', ['get', '_kind'], 'point'],
|
||||
],
|
||||
paint: {
|
||||
'circle-color': '#22c55e',
|
||||
'circle-radius': 6,
|
||||
'circle-color': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
'#f59e0b', // amber for raw OSM
|
||||
'#22c55e', // green for curated
|
||||
],
|
||||
'circle-radius': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
5,
|
||||
6,
|
||||
],
|
||||
'circle-stroke-color': '#ffffff',
|
||||
'circle-stroke-width': 1.5,
|
||||
'circle-opacity': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
0.7,
|
||||
1,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -191,8 +213,18 @@ export function OsmParkBboxMap({
|
||||
source: SOURCE_ID,
|
||||
filter: ['==', ['get', '_kind'], 'polygon'],
|
||||
paint: {
|
||||
'fill-color': '#22c55e',
|
||||
'fill-opacity': 0.18,
|
||||
'fill-color': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
'#f59e0b',
|
||||
'#22c55e',
|
||||
],
|
||||
'fill-opacity': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
0.1,
|
||||
0.18,
|
||||
],
|
||||
},
|
||||
});
|
||||
map.addLayer({
|
||||
@@ -201,9 +233,19 @@ export function OsmParkBboxMap({
|
||||
source: SOURCE_ID,
|
||||
filter: ['==', ['get', '_kind'], 'polygon'],
|
||||
paint: {
|
||||
'line-color': '#22c55e',
|
||||
'line-color': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
'#f59e0b',
|
||||
'#22c55e',
|
||||
],
|
||||
'line-width': 2,
|
||||
'line-opacity': 0.6,
|
||||
'line-opacity': [
|
||||
'case',
|
||||
['==', ['get', 'dataSource'], 'OSM'],
|
||||
0.4,
|
||||
0.6,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -250,6 +292,11 @@ export function OsmParkBboxMap({
|
||||
|
||||
// Initial fetch + listen to viewport changes.
|
||||
void fetchParks();
|
||||
// Wire up the prop-change refetch (used when `includeOsmRaw` flips
|
||||
// — the moveend listener alone doesn't fire on parent re-renders).
|
||||
refetchTokenRef.current = () => {
|
||||
void fetchParks();
|
||||
};
|
||||
});
|
||||
|
||||
map.on('moveend', () => {
|
||||
|
||||
Reference in New Issue
Block a user