288 lines
11 KiB
JavaScript
288 lines
11 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.locationsPublicRouter = exports.locationsAdminRouter = void 0;
|
|
const express_1 = require("express");
|
|
const multer_1 = __importDefault(require("multer"));
|
|
const locations_service_1 = require("./locations.service");
|
|
const locations_schemas_1 = require("./locations.schemas");
|
|
const spatial_1 = require("../../../utils/spatial");
|
|
const database_1 = require("../../../config/database");
|
|
const validate_1 = require("../../../middleware/validate");
|
|
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
|
const roles_1 = require("../../../utils/roles");
|
|
// Multer config for CSV upload (memory storage, 10MB limit)
|
|
const upload = (0, multer_1.default)({
|
|
storage: multer_1.default.memoryStorage(),
|
|
limits: { fileSize: 10 * 1024 * 1024 },
|
|
fileFilter: (_req, file, cb) => {
|
|
if (file.mimetype === 'text/csv' || file.originalname.endsWith('.csv')) {
|
|
cb(null, true);
|
|
}
|
|
else {
|
|
cb(new Error('Only CSV files are allowed'));
|
|
}
|
|
},
|
|
});
|
|
// Multer config for bulk import (100MB limit for large NAR files)
|
|
const bulkUpload = (0, multer_1.default)({
|
|
storage: multer_1.default.memoryStorage(),
|
|
limits: { fileSize: 100 * 1024 * 1024 },
|
|
fileFilter: (_req, file, cb) => {
|
|
if (file.mimetype === 'text/csv' || file.originalname.endsWith('.csv')) {
|
|
cb(null, true);
|
|
}
|
|
else {
|
|
cb(new Error('Only CSV files are allowed'));
|
|
}
|
|
},
|
|
});
|
|
// --- Admin Router ---
|
|
const adminRouter = (0, express_1.Router)();
|
|
exports.locationsAdminRouter = adminRouter;
|
|
adminRouter.use(auth_middleware_1.authenticate);
|
|
adminRouter.use((0, rbac_middleware_1.requireRole)(...roles_1.MAP_ROLES));
|
|
// GET /api/map/locations — list with pagination + filters
|
|
adminRouter.get('/', (0, validate_1.validate)(locations_schemas_1.listLocationsSchema, 'query'), async (req, res, next) => {
|
|
try {
|
|
const result = await locations_service_1.locationsService.findAll(req.query);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// GET /api/map/locations/stats — location statistics
|
|
adminRouter.get('/stats', async (_req, res, next) => {
|
|
try {
|
|
const stats = await locations_service_1.locationsService.getStats();
|
|
res.json(stats);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// GET /api/map/locations/export-csv — export as CSV download
|
|
adminRouter.get('/export-csv', async (_req, res, next) => {
|
|
try {
|
|
const csv = await locations_service_1.locationsService.exportToCsv();
|
|
res.setHeader('Content-Type', 'text/csv');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=locations.csv');
|
|
res.send(csv);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// GET /api/map/locations/all — all geocoded locations for admin map
|
|
adminRouter.get('/all', async (req, res, next) => {
|
|
try {
|
|
const bounds = req.query.minLat ? {
|
|
minLat: parseFloat(req.query.minLat),
|
|
maxLat: parseFloat(req.query.maxLat),
|
|
minLng: parseFloat(req.query.minLng),
|
|
maxLng: parseFloat(req.query.maxLng),
|
|
} : undefined;
|
|
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 5000;
|
|
const locations = await locations_service_1.locationsService.getAllForMap(bounds, limit);
|
|
// Add header if we hit the limit
|
|
if (locations.length === limit) {
|
|
res.setHeader('X-Location-Limit-Hit', 'true');
|
|
}
|
|
res.json(locations);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations/reverse-geocode — reverse geocode lat/lng to address
|
|
adminRouter.post('/reverse-geocode', (0, validate_1.validate)(locations_schemas_1.reverseGeocodeSchema), async (req, res, next) => {
|
|
try {
|
|
const result = await locations_service_1.locationsService.reverseGeocode(req.body.latitude, req.body.longitude);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations/import-bulk — bulk import (NAR or standard CSV)
|
|
adminRouter.post('/import-bulk', bulkUpload.single('file'), async (req, res, next) => {
|
|
// Set 5-minute timeout for large files
|
|
req.setTimeout(5 * 60 * 1000);
|
|
res.setTimeout(5 * 60 * 1000);
|
|
try {
|
|
if (!req.file) {
|
|
res.status(400).json({ error: { message: 'No file uploaded', code: 'NO_FILE' } });
|
|
return;
|
|
}
|
|
const parsed = locations_schemas_1.bulkImportSchema.parse(req.body);
|
|
// Build filters based on filterType
|
|
const filters = {};
|
|
if (parsed.filterType === 'cut' && parsed.cutId) {
|
|
const cut = await database_1.prisma.cut.findUnique({ where: { id: parsed.cutId } });
|
|
if (cut) {
|
|
try {
|
|
filters.cutPolygon = (0, spatial_1.parseGeoJsonPolygon)(cut.geojson);
|
|
}
|
|
catch { /* ignore invalid polygon */ }
|
|
}
|
|
}
|
|
else if (parsed.filterType === 'mapArea') {
|
|
// Load map settings and compute approximate bounding box
|
|
const settings = await database_1.prisma.mapSettings.findFirst();
|
|
if (settings?.latitude && settings?.longitude && settings?.zoom) {
|
|
const lat = parseFloat(settings.latitude.toString());
|
|
const lng = parseFloat(settings.longitude.toString());
|
|
const zoom = settings.zoom;
|
|
// Approximate visible area for a ~1200x800 viewport
|
|
const degreesPerTile = 360 / Math.pow(2, zoom);
|
|
const halfLng = (1200 / 256 / 2) * degreesPerTile;
|
|
const halfLat = (800 / 256 / 2) * degreesPerTile;
|
|
filters.bounds = {
|
|
north: lat + halfLat,
|
|
south: lat - halfLat,
|
|
east: lng + halfLng,
|
|
west: lng - halfLng,
|
|
};
|
|
}
|
|
}
|
|
else if (parsed.filterType === 'city' && parsed.filterCity) {
|
|
filters.city = parsed.filterCity;
|
|
}
|
|
else if (parsed.filterType === 'province' && parsed.filterProvince) {
|
|
filters.province = parsed.filterProvince;
|
|
}
|
|
const result = await locations_service_1.locationsService.importBulk(req.file.buffer, req.user.id, parsed, filters);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// GET /api/map/locations/:id — get single location
|
|
adminRouter.get('/:id', async (req, res, next) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const location = await locations_service_1.locationsService.findById(id);
|
|
res.json(location);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// GET /api/map/locations/:id/history — get location edit history
|
|
adminRouter.get('/:id/history', async (req, res, next) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const page = req.query.page ? parseInt(req.query.page, 10) : 1;
|
|
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 20;
|
|
const result = await locations_service_1.locationsService.getHistory(id, page, limit);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations — create location
|
|
adminRouter.post('/', (0, validate_1.validate)(locations_schemas_1.createLocationSchema), async (req, res, next) => {
|
|
try {
|
|
const location = await locations_service_1.locationsService.create(req.body, req.user.id);
|
|
res.status(201).json(location);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// PUT /api/map/locations/:id — update location
|
|
adminRouter.put('/:id', (0, validate_1.validate)(locations_schemas_1.updateLocationSchema), async (req, res, next) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const location = await locations_service_1.locationsService.update(id, req.body, req.user.id);
|
|
res.json(location);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// DELETE /api/map/locations/:id — delete location
|
|
adminRouter.delete('/:id', async (req, res, next) => {
|
|
try {
|
|
const id = req.params.id;
|
|
await locations_service_1.locationsService.delete(id);
|
|
res.status(204).send();
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations/bulk-delete — bulk delete
|
|
adminRouter.post('/bulk-delete', (0, validate_1.validate)(locations_schemas_1.bulkDeleteSchema), async (req, res, next) => {
|
|
try {
|
|
const result = await locations_service_1.locationsService.bulkDelete(req.body.ids);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations/geocode — geocode an address
|
|
adminRouter.post('/geocode', (0, validate_1.validate)(locations_schemas_1.geocodeAddressSchema), async (req, res, next) => {
|
|
try {
|
|
const result = await locations_service_1.locationsService.geocodeAddress(req.body.address);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations/geocode-missing — geocode all ungeocoded locations
|
|
adminRouter.post('/geocode-missing', async (_req, res, next) => {
|
|
try {
|
|
const result = await locations_service_1.locationsService.geocodeMissing();
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// POST /api/map/locations/import-csv — upload + import CSV
|
|
adminRouter.post('/import-csv', upload.single('file'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) {
|
|
res.status(400).json({ error: { message: 'No file uploaded', code: 'NO_FILE' } });
|
|
return;
|
|
}
|
|
const result = await locations_service_1.locationsService.importFromCsv(req.file.buffer, req.user.id);
|
|
res.json(result);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
// --- Public Router ---
|
|
const publicRouter = (0, express_1.Router)();
|
|
exports.locationsPublicRouter = publicRouter;
|
|
// GET /api/map/locations/public — all locations for map (no PII)
|
|
publicRouter.get('/public', async (req, res, next) => {
|
|
try {
|
|
const bounds = req.query.minLat ? {
|
|
minLat: parseFloat(req.query.minLat),
|
|
maxLat: parseFloat(req.query.maxLat),
|
|
minLng: parseFloat(req.query.minLng),
|
|
maxLng: parseFloat(req.query.maxLng),
|
|
} : undefined;
|
|
const locations = await locations_service_1.locationsService.getPublicLocations(bounds);
|
|
// Add header if we hit the safety limit
|
|
if (locations.length === 5000) {
|
|
res.setHeader('X-Location-Limit-Hit', 'true');
|
|
}
|
|
res.json(locations);
|
|
}
|
|
catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
//# sourceMappingURL=locations.routes.js.map
|