"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.cutsService = void 0; const client_1 = require("@prisma/client"); const database_1 = require("../../../config/database"); const error_handler_1 = require("../../../middleware/error-handler"); const spatial_1 = require("../../../utils/spatial"); const logger_1 = require("../../../utils/logger"); exports.cutsService = { async findAll(filters) { const { page, limit, category, search } = filters; const skip = (page - 1) * limit; const where = {}; if (search) { where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, ]; } if (category) where.category = category; const [cuts, total] = await Promise.all([ database_1.prisma.cut.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, }), database_1.prisma.cut.count({ where }), ]); return { cuts, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; }, async findById(id) { const cut = await database_1.prisma.cut.findUnique({ where: { id } }); if (!cut) { throw new error_handler_1.AppError(404, 'Cut not found', 'CUT_NOT_FOUND'); } return cut; }, async create(data, userId) { // Auto-calculate bounds from geojson if not provided let boundsStr = data.bounds; if (!boundsStr) { try { const rings = (0, spatial_1.parseGeoJsonPolygon)(data.geojson); const allCoords = rings.flat(); const bounds = (0, spatial_1.calculateBounds)(allCoords); boundsStr = JSON.stringify(bounds); } catch { // Bounds calculation optional } } const cut = await database_1.prisma.cut.create({ data: { name: data.name, description: data.description, color: data.color, opacity: data.opacity, category: data.category, isPublic: data.isPublic, isOfficial: data.isOfficial, geojson: data.geojson, bounds: boundsStr, showLocations: data.showLocations, exportEnabled: data.exportEnabled, assignedTo: data.assignedTo, createdByUserId: userId, }, }); return cut; }, async update(id, data) { const existing = await database_1.prisma.cut.findUnique({ where: { id } }); if (!existing) { throw new error_handler_1.AppError(404, 'Cut not found', 'CUT_NOT_FOUND'); } // Recalculate bounds if geojson changed const updateData = { ...data }; if (data.geojson && !data.bounds) { try { const rings = (0, spatial_1.parseGeoJsonPolygon)(data.geojson); const allCoords = rings.flat(); const bounds = (0, spatial_1.calculateBounds)(allCoords); updateData.bounds = JSON.stringify(bounds); } catch { // Bounds calculation optional } } if (data.lastCanvassed !== undefined) { updateData.lastCanvassed = data.lastCanvassed ? new Date(data.lastCanvassed) : null; } const cut = await database_1.prisma.cut.update({ where: { id }, data: updateData, }); return cut; }, async delete(id) { const existing = await database_1.prisma.cut.findUnique({ where: { id } }); if (!existing) { throw new error_handler_1.AppError(404, 'Cut not found', 'CUT_NOT_FOUND'); } await database_1.prisma.cut.delete({ where: { id } }); }, async getPublicCuts() { const cuts = await database_1.prisma.cut.findMany({ where: { isPublic: true }, select: { id: true, name: true, description: true, color: true, opacity: true, category: true, geojson: true, bounds: true, }, orderBy: { name: 'asc' }, }); return cuts; }, async getLocationsInCut(id) { const cut = await database_1.prisma.cut.findUnique({ where: { id } }); if (!cut) { throw new error_handler_1.AppError(404, 'Cut not found', 'CUT_NOT_FOUND'); } const rings = (0, spatial_1.parseGeoJsonPolygon)(cut.geojson); // Get all locations (latitude/longitude are non-nullable) const locations = await database_1.prisma.location.findMany(); // Filter to those inside the polygon const inside = locations.filter((loc) => { const lat = Number(loc.latitude); const lng = Number(loc.longitude); return rings.some((ring) => (0, spatial_1.isPointInPolygon)(lat, lng, ring)); }); return inside; }, async exportSingleGeoJson(id) { const cut = await database_1.prisma.cut.findUnique({ where: { id } }); if (!cut) throw new error_handler_1.AppError(404, 'Cut not found', 'CUT_NOT_FOUND'); const geojson = JSON.parse(cut.geojson); return { type: 'Feature', properties: { name: cut.name, description: cut.description, color: cut.color, 'fill-opacity': Number(cut.opacity), fill: cut.color, category: cut.category, isPublic: cut.isPublic, isOfficial: cut.isOfficial, assignedTo: cut.assignedTo, }, geometry: geojson, }; }, async exportAllGeoJson() { const cuts = await database_1.prisma.cut.findMany({ orderBy: { name: 'asc' } }); const features = cuts.map((cut) => { const geojson = JSON.parse(cut.geojson); return { type: 'Feature', properties: { name: cut.name, description: cut.description, color: cut.color, 'fill-opacity': Number(cut.opacity), fill: cut.color, category: cut.category, isPublic: cut.isPublic, isOfficial: cut.isOfficial, assignedTo: cut.assignedTo, }, geometry: geojson, }; }); return { type: 'FeatureCollection', features, }; }, async importGeoJson(buffer, userId) { let parsed; try { parsed = JSON.parse(buffer.toString('utf-8')); } catch { throw new error_handler_1.AppError(400, 'Invalid JSON file', 'INVALID_JSON'); } // Collect features from various GeoJSON shapes const features = []; const type = parsed.type; if (type === 'FeatureCollection' && Array.isArray(parsed.features)) { for (const f of parsed.features) { if (f.geometry) features.push({ geometry: f.geometry, properties: (f.properties ?? {}) }); } } else if (type === 'Feature' && parsed.geometry) { features.push({ geometry: parsed.geometry, properties: (parsed.properties ?? {}) }); } else if (type === 'Polygon' || type === 'MultiPolygon') { features.push({ geometry: parsed, properties: {} }); } else { throw new error_handler_1.AppError(400, 'Unsupported GeoJSON type. Expected Feature, FeatureCollection, Polygon, or MultiPolygon.', 'UNSUPPORTED_GEOJSON'); } let success = 0; let failed = 0; const errors = []; for (let i = 0; i < features.length; i++) { try { const { geometry, properties } = features[i]; const geoType = geometry.type; if (geoType !== 'Polygon' && geoType !== 'MultiPolygon') { errors.push(`Feature ${i + 1}: Unsupported geometry type "${geoType}"`); failed++; continue; } const geojsonStr = JSON.stringify(geometry); // Extract properties const name = properties?.name || `Imported Cut ${i + 1}`; const color = properties?.color || properties?.fill || '#3388ff'; const opacity = typeof properties?.['fill-opacity'] === 'number' ? properties['fill-opacity'] : 0.3; const categoryRaw = properties?.category?.toUpperCase(); const category = categoryRaw && Object.values(client_1.CutCategory).includes(categoryRaw) ? categoryRaw : undefined; // Calculate bounds let boundsStr; try { const rings = (0, spatial_1.parseGeoJsonPolygon)(geojsonStr); const allCoords = rings.flat(); const bounds = (0, spatial_1.calculateBounds)(allCoords); boundsStr = JSON.stringify(bounds); } catch { /* optional */ } await database_1.prisma.cut.create({ data: { name, description: properties?.description || undefined, color: /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#3388ff', opacity, category, isPublic: properties?.isPublic === true, isOfficial: properties?.isOfficial === true, geojson: geojsonStr, bounds: boundsStr, assignedTo: properties?.assignedTo || undefined, createdByUserId: userId, }, }); success++; } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error'; errors.push(`Feature ${i + 1}: ${msg}`); failed++; logger_1.logger.warn(`GeoJSON import feature ${i + 1} failed:`, err); } } return { total: features.length, success, failed, errors: errors.slice(0, 50) }; }, async getStatistics(id) { const cut = await database_1.prisma.cut.findUnique({ where: { id } }); if (!cut) { throw new error_handler_1.AppError(404, 'Cut not found', 'CUT_NOT_FOUND'); } const rings = (0, spatial_1.parseGeoJsonPolygon)(cut.geojson); // Get all locations with addresses (latitude/longitude are non-nullable) const locations = await database_1.prisma.location.findMany({ select: { id: true, latitude: true, longitude: true, addresses: { select: { id: true, supportLevel: true, sign: true, }, }, }, }); // Filter locations inside polygon and flatten addresses const addressesInCut = []; for (const loc of locations) { const lat = Number(loc.latitude); const lng = Number(loc.longitude); if (rings.some((ring) => (0, spatial_1.isPointInPolygon)(lat, lng, ring))) { addressesInCut.push(...loc.addresses); } } const byLevel = { LEVEL_1: 0, LEVEL_2: 0, LEVEL_3: 0, LEVEL_4: 0, NONE: 0, }; let withSign = 0; for (const addr of addressesInCut) { const level = addr.supportLevel || 'NONE'; byLevel[level] = (byLevel[level] || 0) + 1; if (addr.sign) withSign++; } return { total: addressesInCut.length, byLevel, withSign, }; }, }; //# sourceMappingURL=cuts.service.js.map