327 lines
12 KiB
JavaScript
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
|