289 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 client_1 = require("@prisma/client");
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 MAP_ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN];
// 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)(...MAP_ADMIN_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