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:
@@ -35,8 +35,10 @@ import {
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import {
|
||||
industrialApi,
|
||||
REGION_LABELS,
|
||||
type OsmPendingItem,
|
||||
type OsmPendingResult,
|
||||
type VietnamRegion,
|
||||
} from '@/lib/khu-cong-nghiep-api';
|
||||
|
||||
/**
|
||||
@@ -56,6 +58,17 @@ import {
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
/** Buckets for the "Diện tích tối thiểu" filter. 50 ha is the default
|
||||
* because most "real" KCN start there — anything below tends to be a
|
||||
* single factory or warehouse mistagged as `landuse=industrial`. */
|
||||
const MIN_AREA_OPTIONS: { value: number; label: string }[] = [
|
||||
{ value: 0, label: 'Tất cả' },
|
||||
{ value: 5, label: '≥ 5 ha' },
|
||||
{ value: 50, label: '≥ 50 ha (KCN nhỏ)' },
|
||||
{ value: 200, label: '≥ 200 ha (KCN lớn)' },
|
||||
{ value: 500, label: '≥ 500 ha (KCN trọng điểm)' },
|
||||
];
|
||||
|
||||
const QUICK_LOCK_FIELDS: { key: string; label: string }[] = [
|
||||
{ key: 'name', label: 'Tên KCN' },
|
||||
{ key: 'developer', label: 'Chủ đầu tư' },
|
||||
@@ -91,6 +104,8 @@ export default function AdminOsmReviewPage() {
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [provinceFilter, setProvinceFilter] = useState('');
|
||||
const [minAreaHa, setMinAreaHa] = useState<number>(50);
|
||||
const [region, setRegion] = useState<VietnamRegion | ''>('');
|
||||
|
||||
// Promote dialog state
|
||||
const [promoteTarget, setPromoteTarget] = useState<OsmPendingItem | null>(null);
|
||||
@@ -107,6 +122,8 @@ export default function AdminOsmReviewPage() {
|
||||
limit: PAGE_SIZE,
|
||||
q: search || undefined,
|
||||
province: provinceFilter || undefined,
|
||||
minAreaHa,
|
||||
region: region || undefined,
|
||||
});
|
||||
setResult(data);
|
||||
} catch (e) {
|
||||
@@ -114,7 +131,7 @@ export default function AdminOsmReviewPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, search, provinceFilter]);
|
||||
}, [page, search, provinceFilter, minAreaHa, region]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchQueue();
|
||||
@@ -218,6 +235,43 @@ export default function AdminOsmReviewPage() {
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-40">
|
||||
<label className="mb-1 block text-xs text-foreground-dim">
|
||||
Diện tích tối thiểu
|
||||
</label>
|
||||
<select
|
||||
value={minAreaHa}
|
||||
onChange={(e) => {
|
||||
setPage(1);
|
||||
setMinAreaHa(Number(e.target.value));
|
||||
}}
|
||||
className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm"
|
||||
>
|
||||
{MIN_AREA_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<label className="mb-1 block text-xs text-foreground-dim">Vùng miền</label>
|
||||
<select
|
||||
value={region}
|
||||
onChange={(e) => {
|
||||
setPage(1);
|
||||
setRegion((e.target.value as VietnamRegion) || '');
|
||||
}}
|
||||
className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm"
|
||||
>
|
||||
<option value="">Tất cả</option>
|
||||
{(Object.keys(REGION_LABELS) as VietnamRegion[]).map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{REGION_LABELS[r]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-40">
|
||||
<label className="mb-1 block text-xs text-foreground-dim">Tỉnh / TP</label>
|
||||
<Input
|
||||
@@ -234,7 +288,7 @@ export default function AdminOsmReviewPage() {
|
||||
<Search className="mr-1.5 h-3.5 w-3.5" />
|
||||
Tìm
|
||||
</Button>
|
||||
{(search || provinceFilter) && (
|
||||
{(search || provinceFilter || region || minAreaHa !== 50) && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
@@ -243,11 +297,13 @@ export default function AdminOsmReviewPage() {
|
||||
setSearchInput('');
|
||||
setSearch('');
|
||||
setProvinceFilter('');
|
||||
setRegion('');
|
||||
setMinAreaHa(50);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<X className="mr-1 h-3 w-3" />
|
||||
Xóa bộ lọc
|
||||
Đặt lại
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
@@ -444,8 +500,9 @@ export default function AdminOsmReviewPage() {
|
||||
<span className="flex items-start gap-2 text-sm">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>
|
||||
KCN <strong>{promoteTarget?.name}</strong> sẽ được chuyển sang trạng thái public
|
||||
(OSM_PROMOTED). Chọn các trường muốn khóa để bảo vệ chúng khỏi OSM sync sau này.
|
||||
Sắp promote: <strong>{promoteTarget?.name}</strong>. KCN sẽ chuyển sang trạng thái
|
||||
public (OSM_PROMOTED). Chọn các trường muốn khóa để bảo vệ chúng khỏi OSM sync sau
|
||||
này.
|
||||
</span>
|
||||
</span>
|
||||
</DialogDescription>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Factory, Map as MapIcon, List, Columns } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { OsmMapLegend } from '@/components/khu-cong-nghiep/osm-map-legend';
|
||||
import { OsmParkBboxMap } from '@/components/khu-cong-nghiep/osm-park-bbox-map';
|
||||
import { ParkCard } from '@/components/khu-cong-nghiep/park-card';
|
||||
import { ParkFilterBar } from '@/components/khu-cong-nghiep/park-filter-bar';
|
||||
@@ -19,6 +20,10 @@ export default function KhuCongNghiepPage() {
|
||||
limit: PAGE_SIZE,
|
||||
});
|
||||
const [viewMode, setViewMode] = React.useState<ViewMode>('split');
|
||||
// When true, the bbox map also shows raw OSM-imported parks (amber
|
||||
// markers) on top of the curated catalog. Off by default — most users
|
||||
// want only the verified set.
|
||||
const [includeOsmRaw, setIncludeOsmRaw] = React.useState(false);
|
||||
|
||||
const { data, isLoading, isError } = useIndustrialParksSearch(filters);
|
||||
|
||||
@@ -113,7 +118,16 @@ export default function KhuCongNghiepPage() {
|
||||
|
||||
{/* Map-only view — bbox-driven, loads ALL parks in viewport */}
|
||||
{viewMode === 'map' && (
|
||||
<OsmParkBboxMap className="h-[calc(100vh-260px)]" />
|
||||
<>
|
||||
<OsmMapLegend
|
||||
includeOsmRaw={includeOsmRaw}
|
||||
onToggleOsmRaw={setIncludeOsmRaw}
|
||||
/>
|
||||
<OsmParkBboxMap
|
||||
className="h-[calc(100vh-300px)]"
|
||||
includeOsmRaw={includeOsmRaw}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Split view: list left, sticky bbox map right (lg+ only) */}
|
||||
@@ -126,8 +140,16 @@ export default function KhuCongNghiepPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<OsmParkBboxMap className="sticky top-20 h-[calc(100vh-220px)]" />
|
||||
<div className="hidden lg:flex lg:flex-col lg:gap-2">
|
||||
<OsmMapLegend
|
||||
includeOsmRaw={includeOsmRaw}
|
||||
onToggleOsmRaw={setIncludeOsmRaw}
|
||||
compact
|
||||
/>
|
||||
<OsmParkBboxMap
|
||||
className="sticky top-20 h-[calc(100vh-260px)]"
|
||||
includeOsmRaw={includeOsmRaw}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user