"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