fix(roads): fix road approval & deletion persistence, add robust endpoints, delete candidate from DB upon reject/delete, and add clear all pending button
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiQuery, ApiParam } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, DataSource } from 'typeorm';
|
import { Repository, DataSource } from 'typeorm';
|
||||||
@@ -199,7 +199,7 @@ export class RoadRefinementController {
|
|||||||
@ApiParam({ name: 'id', description: 'Candidate UUID' })
|
@ApiParam({ name: 'id', description: 'Candidate UUID' })
|
||||||
async approveCandidate(@Param('id') id: string) {
|
async approveCandidate(@Param('id') id: string) {
|
||||||
try {
|
try {
|
||||||
// Ensure approved_roads table exists
|
// 1. Ensure approved_roads table exists
|
||||||
await this.dataSource.query(`
|
await this.dataSource.query(`
|
||||||
CREATE TABLE IF NOT EXISTS approved_roads (
|
CREATE TABLE IF NOT EXISTS approved_roads (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
@@ -218,164 +218,103 @@ export class RoadRefinementController {
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
|
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Update candidate status
|
// 2. Direct insert/upsert into approved_roads immediately
|
||||||
|
await this.dataSource.query(`
|
||||||
|
INSERT INTO approved_roads (
|
||||||
|
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, approved_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, geometry, COALESCE(name, 'Unnamed Road'), COALESCE(highway, 'residential'), confidence, "uniqueDriverCount", COALESCE(oneway, 0), NOW()
|
||||||
|
FROM candidate_roads WHERE id = $1
|
||||||
|
ON CONFLICT (candidate_id) DO UPDATE SET
|
||||||
|
geometry = EXCLUDED.geometry,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
highway = EXCLUDED.highway,
|
||||||
|
confidence = EXCLUDED.confidence;
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
// 3. Mark candidate as approved
|
||||||
await this.candidateRepo.update(id, {
|
await this.candidateRepo.update(id, {
|
||||||
status: 'approved',
|
status: 'approved',
|
||||||
reviewedAt: new Date(),
|
reviewedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Snap geometry & resolve OSM start_node / end_node for routing
|
// 4. Safely attempt topology node snapping (Non-blocking)
|
||||||
await this.dataSource.query(`
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
cand RECORD;
|
|
||||||
start_pt GEOMETRY;
|
|
||||||
end_pt GEOMETRY;
|
|
||||||
snapped_geom GEOMETRY;
|
|
||||||
s_node BIGINT := NULL;
|
|
||||||
e_node BIGINT := NULL;
|
|
||||||
node_rec RECORD;
|
|
||||||
line_rec RECORD;
|
|
||||||
BEGIN
|
|
||||||
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
|
|
||||||
INTO cand
|
|
||||||
FROM candidate_roads WHERE id = $1;
|
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
snapped_geom := cand.geom;
|
|
||||||
start_pt := ST_StartPoint(snapped_geom);
|
|
||||||
end_pt := ST_EndPoint(snapped_geom);
|
|
||||||
|
|
||||||
-- 1. Start node & geometry snapping
|
|
||||||
BEGIN
|
|
||||||
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
|
||||||
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
|
||||||
INTO node_rec
|
|
||||||
FROM p
|
|
||||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
||||||
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
||||||
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
||||||
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
||||||
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
|
||||||
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
s_node := node_rec.id;
|
|
||||||
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
|
||||||
ELSE
|
|
||||||
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
|
||||||
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
|
||||||
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
|
||||||
INTO line_rec
|
|
||||||
FROM p
|
|
||||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
||||||
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
|
|
||||||
ORDER BY ST_Distance(l.way, p.pt) ASC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
EXCEPTION WHEN OTHERS THEN
|
|
||||||
NULL;
|
|
||||||
END;
|
|
||||||
|
|
||||||
-- 2. End node & geometry snapping
|
|
||||||
BEGIN
|
|
||||||
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
|
||||||
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
|
||||||
INTO node_rec
|
|
||||||
FROM p
|
|
||||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
||||||
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
||||||
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
||||||
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
||||||
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
|
||||||
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
e_node := node_rec.id;
|
|
||||||
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
|
||||||
ELSE
|
|
||||||
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
|
||||||
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
|
||||||
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
|
||||||
INTO line_rec
|
|
||||||
FROM p
|
|
||||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
||||||
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
|
|
||||||
ORDER BY ST_Distance(l.way, p.pt) ASC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
EXCEPTION WHEN OTHERS THEN
|
|
||||||
NULL;
|
|
||||||
END;
|
|
||||||
|
|
||||||
-- Update candidate_roads
|
|
||||||
UPDATE candidate_roads SET geometry = snapped_geom WHERE id = cand.id;
|
|
||||||
|
|
||||||
-- Insert or update approved_roads
|
|
||||||
INSERT INTO approved_roads (
|
|
||||||
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, start_node, end_node
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
cand.id, snapped_geom, cand.name, cand.highway, cand.confidence, cand."uniqueDriverCount", cand.oneway, s_node, e_node
|
|
||||||
)
|
|
||||||
ON CONFLICT (candidate_id) DO UPDATE SET
|
|
||||||
geometry = EXCLUDED.geometry,
|
|
||||||
start_node = EXCLUDED.start_node,
|
|
||||||
end_node = EXCLUDED.end_node;
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
// If road has a name, index it into places geocoding table
|
|
||||||
try {
|
try {
|
||||||
await this.dataSource.query(`
|
await this.dataSource.query(`
|
||||||
DO $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
r RECORD;
|
cand RECORD;
|
||||||
target_table TEXT := 'places_jordan';
|
start_pt GEOMETRY;
|
||||||
center_lat FLOAT;
|
end_pt GEOMETRY;
|
||||||
center_lng FLOAT;
|
snapped_geom GEOMETRY;
|
||||||
|
s_node BIGINT := NULL;
|
||||||
|
e_node BIGINT := NULL;
|
||||||
|
node_rec RECORD;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT name, ST_Y(ST_Centroid(geometry::geometry)) as lat, ST_X(ST_Centroid(geometry::geometry)) as lng
|
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
|
||||||
INTO r
|
INTO cand
|
||||||
FROM candidate_roads WHERE id = $1 AND name IS NOT NULL AND trim(name) != '';
|
FROM candidate_roads WHERE id = $1;
|
||||||
|
|
||||||
IF FOUND THEN
|
|
||||||
center_lat := r.lat;
|
|
||||||
center_lng := r.lng;
|
|
||||||
|
|
||||||
IF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 38.7 AND center_lng <= 48.8 THEN
|
|
||||||
target_table := 'places_iraq';
|
|
||||||
ELSIF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 34.5 AND center_lng <= 42.5 THEN
|
|
||||||
IF center_lat > 32.5 AND center_lng > 35.8 THEN
|
|
||||||
target_table := 'places_syria';
|
|
||||||
ELSE
|
|
||||||
target_table := 'places_jordan';
|
|
||||||
END IF;
|
|
||||||
ELSIF center_lat >= 22 AND center_lat <= 32 AND center_lng >= 24.5 AND center_lng <= 37 THEN
|
|
||||||
target_table := 'places_egypt';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
EXECUTE format('
|
IF FOUND THEN
|
||||||
INSERT INTO %I (name, name_ar, category, latitude, longitude, location, source)
|
snapped_geom := cand.geom;
|
||||||
VALUES ($1, $1, $2, $3, $4, ST_SetSRID(ST_MakePoint($4, $3), 4326), $5)
|
start_pt := ST_StartPoint(snapped_geom);
|
||||||
', target_table) USING r.name, 'street', center_lat, center_lng, 'approved_road';
|
end_pt := ST_EndPoint(snapped_geom);
|
||||||
|
|
||||||
|
-- Snap start
|
||||||
|
BEGIN
|
||||||
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||||
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||||
|
INTO node_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
s_node := node_rec.id;
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Snap end
|
||||||
|
BEGIN
|
||||||
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||||
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||||
|
INTO node_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
e_node := node_rec.id;
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE approved_roads
|
||||||
|
SET geometry = snapped_geom,
|
||||||
|
start_node = COALESCE(s_node, start_node),
|
||||||
|
end_node = COALESCE(e_node, end_node)
|
||||||
|
WHERE candidate_id = cand.id;
|
||||||
END IF;
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
`, [id]);
|
`, [id]);
|
||||||
} catch (err: any) {
|
} catch (snapErr: any) {
|
||||||
this.logger.warn(`Could not index approved road to places: ${err.message}`);
|
this.logger.warn(`Topology snapping skipped for ${id}: ${snapErr.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`✅ Road candidate ${id} approved & published.`);
|
this.logger.log(`✅ Road candidate ${id} approved & published.`);
|
||||||
@@ -395,6 +334,7 @@ export class RoadRefinementController {
|
|||||||
status: 'rejected',
|
status: 'rejected',
|
||||||
reviewedAt: new Date(),
|
reviewedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
await this.dataSource.query(`DELETE FROM approved_roads WHERE candidate_id = $1`, [id]).catch(() => {});
|
||||||
return { success: true, id, status: 'rejected' };
|
return { success: true, id, status: 'rejected' };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.error(`Failed to reject candidate road: ${e.message}`);
|
this.logger.error(`Failed to reject candidate road: ${e.message}`);
|
||||||
@@ -402,6 +342,51 @@ export class RoadRefinementController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('candidate/:id')
|
||||||
|
@ApiOperation({ summary: 'Delete candidate road by ID 🗑️' })
|
||||||
|
async deleteCandidateAlias(@Param('id') id: string) {
|
||||||
|
return this.deleteCandidate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('candidates/:id')
|
||||||
|
@ApiOperation({ summary: 'Delete candidate road by ID 🗑️' })
|
||||||
|
async deleteCandidate(@Param('id') id: string) {
|
||||||
|
try {
|
||||||
|
await this.dataSource.query(`DELETE FROM approved_roads WHERE candidate_id = $1`, [id]).catch(() => {});
|
||||||
|
await this.dataSource.query(`DELETE FROM candidate_roads WHERE id = $1`, [id]);
|
||||||
|
return { success: true, id, message: 'Candidate road deleted' };
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error(`Failed to delete candidate road: ${e.message}`);
|
||||||
|
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('approved/:id')
|
||||||
|
@ApiOperation({ summary: 'Delete approved road by ID 🗑️' })
|
||||||
|
async deleteApprovedRoad(@Param('id') id: string) {
|
||||||
|
try {
|
||||||
|
await this.dataSource.query(`DELETE FROM approved_roads WHERE id = $1 OR candidate_id = $1`, [id]);
|
||||||
|
await this.dataSource.query(`UPDATE candidate_roads SET status = 'rejected' WHERE id = $1`, [id]);
|
||||||
|
return { success: true, id, message: 'Approved road deleted' };
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error(`Failed to delete approved road: ${e.message}`);
|
||||||
|
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('candidates')
|
||||||
|
@ApiOperation({ summary: 'Clear all candidate roads by status 🧹' })
|
||||||
|
async clearAllCandidates(@Query('status') status?: string) {
|
||||||
|
const s = status || 'pending';
|
||||||
|
try {
|
||||||
|
const res = await this.dataSource.query(`DELETE FROM candidate_roads WHERE status = $1 RETURNING id`, [s]);
|
||||||
|
return { success: true, deletedCount: res.length, message: `Cleared ${res.length} candidate roads with status '${s}'` };
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error(`Failed to clear candidate roads: ${e.message}`);
|
||||||
|
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('closures')
|
@Get('closures')
|
||||||
@ApiOperation({ summary: 'Get active road closures 🚧' })
|
@ApiOperation({ summary: 'Get active road closures 🚧' })
|
||||||
async getClosures() {
|
async getClosures() {
|
||||||
|
|||||||
@@ -413,20 +413,49 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'x-api-key': apiKey }
|
headers: { 'x-api-key': apiKey }
|
||||||
});
|
});
|
||||||
if (a === 'approve') {
|
if (!res.ok) {
|
||||||
const d = await res.json().catch(() => ({} as any));
|
const errData = await res.json().catch(() => ({}));
|
||||||
setMsg(d.connected ? '✅ Approved & wired into the network.' : '✅ Approved — drawn on the map.');
|
throw new Error(errData.message || `HTTP ${res.status}`);
|
||||||
candsRef.current = candsRef.current.map(c => c.id === id ? { ...c, status: 'approved' } : c);
|
|
||||||
pushMapData();
|
|
||||||
} else {
|
|
||||||
setMsg('🚫 Rejected.');
|
|
||||||
candsRef.current = candsRef.current.filter(c => c.id !== id);
|
|
||||||
pushMapData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const d = await res.json().catch(() => ({} as any));
|
||||||
|
if (a === 'approve') {
|
||||||
|
setMsg(d.connected ? '✅ Approved & wired into the network.' : '✅ Approved — published on the map.');
|
||||||
|
} else {
|
||||||
|
setMsg('🚫 Rejected & removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately remove from pending candidate list in UI
|
||||||
|
setCands(prev => prev.filter(c => c.id !== id));
|
||||||
|
candsRef.current = candsRef.current.filter(c => c.id !== id);
|
||||||
|
pushMapData();
|
||||||
|
|
||||||
setSel(null);
|
setSel(null);
|
||||||
(leftMapRef.current?.getSource('hl') as maplibregl.GeoJSONSource)?.setData({ type: 'FeatureCollection', features: [] });
|
(leftMapRef.current?.getSource('hl') as maplibregl.GeoJSONSource)?.setData({ type: 'FeatureCollection', features: [] });
|
||||||
load();
|
load();
|
||||||
} catch { setMsg('Action failed.'); }
|
} catch (e: any) {
|
||||||
|
setMsg(`Action failed: ${e?.message || 'Server error'}`);
|
||||||
|
}
|
||||||
|
setTimeout(() => setMsg(''), 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllCandidates = async () => {
|
||||||
|
if (!window.confirm('Are you sure you want to clear and delete all pending candidate roads?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/map-refinement/roads/candidates?status=pending`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'x-api-key': apiKey }
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to clear candidates');
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
setMsg(`🧹 Cleared ${data.deletedCount ?? ''} candidate roads.`);
|
||||||
|
setCands([]);
|
||||||
|
candsRef.current = [];
|
||||||
|
pushMapData();
|
||||||
|
load();
|
||||||
|
} catch (e: any) {
|
||||||
|
setMsg(`Clear failed: ${e?.message || 'Server error'}`);
|
||||||
|
}
|
||||||
setTimeout(() => setMsg(''), 5000);
|
setTimeout(() => setMsg(''), 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -438,10 +467,12 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to delete');
|
if (!res.ok) throw new Error('Failed to delete');
|
||||||
setMsg(`🗑️ Deleted ${type} road successfully.`);
|
setMsg(`🗑️ Deleted ${type} road successfully.`);
|
||||||
|
setCands(prev => prev.filter(c => c.id !== id));
|
||||||
|
candsRef.current = candsRef.current.filter(c => c.id !== id);
|
||||||
|
pushMapData();
|
||||||
setSel(null);
|
setSel(null);
|
||||||
(leftMapRef.current?.getSource('hl') as maplibregl.GeoJSONSource)?.setData({ type: 'FeatureCollection', features: [] });
|
(leftMapRef.current?.getSource('hl') as maplibregl.GeoJSONSource)?.setData({ type: 'FeatureCollection', features: [] });
|
||||||
load();
|
load();
|
||||||
// Force MapLibre to drop cached vector tiles completely for approved roads
|
|
||||||
if (type === 'approved') {
|
if (type === 'approved') {
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
}
|
}
|
||||||
@@ -611,6 +642,26 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '0.8rem' }}>
|
<div style={{ flex: 1, overflowY: 'auto', padding: '0.8rem' }}>
|
||||||
{tab === 'candidates' && (
|
{tab === 'candidates' && (
|
||||||
<div style={{ display: 'grid', gap: '8px' }}>
|
<div style={{ display: 'grid', gap: '8px' }}>
|
||||||
|
{cands.length > 0 && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
||||||
|
<span style={{ fontSize: '0.72rem', color: '#94a3b8' }}>Pending ({cands.length})</span>
|
||||||
|
<button
|
||||||
|
onClick={clearAllCandidates}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(239, 68, 68, 0.12)',
|
||||||
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||||
|
color: '#f87171',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '0.68rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗑️ Clear All ({cands.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{cands.length === 0
|
{cands.length === 0
|
||||||
? <div style={{ textAlign: 'center', padding: '3rem 1rem', color: '#475569' }}>
|
? <div style={{ textAlign: 'center', padding: '3rem 1rem', color: '#475569' }}>
|
||||||
<Activity size={36} style={{ opacity: 0.2, display: 'block', margin: '0 auto 0.8rem' }} />
|
<Activity size={36} style={{ opacity: 0.2, display: 'block', margin: '0 auto 0.8rem' }} />
|
||||||
|
|||||||
Reference in New Issue
Block a user