327 lines
12 KiB
JavaScript

"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