260 lines
11 KiB
JavaScript
260 lines
11 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.trackingService = void 0;
|
|
const client_1 = require("@prisma/client");
|
|
const database_1 = require("../../../config/database");
|
|
const spatial_1 = require("../../../utils/spatial");
|
|
const logger_1 = require("../../../utils/logger");
|
|
const MAX_DISTANCE_JUMP_M = 1000; // Skip distance calc for GPS glitches > 1km
|
|
class TrackingService {
|
|
/** Start or resume a tracking session for a volunteer. */
|
|
async startSession(userId, data) {
|
|
// Check for existing active session — reuse it
|
|
const existing = await database_1.prisma.trackingSession.findFirst({
|
|
where: { userId, isActive: true },
|
|
});
|
|
if (existing)
|
|
return existing;
|
|
return database_1.prisma.trackingSession.create({
|
|
data: {
|
|
userId,
|
|
canvassSessionId: data.canvassSessionId ?? null,
|
|
lastLatitude: data.latitude != null ? new client_1.Prisma.Decimal(data.latitude) : null,
|
|
lastLongitude: data.longitude != null ? new client_1.Prisma.Decimal(data.longitude) : null,
|
|
lastRecordedAt: data.latitude != null ? new Date() : null,
|
|
},
|
|
});
|
|
}
|
|
/** End a tracking session. */
|
|
async endSession(sessionId, userId) {
|
|
const session = await database_1.prisma.trackingSession.findFirst({
|
|
where: { id: sessionId, userId, isActive: true },
|
|
});
|
|
if (!session)
|
|
return null;
|
|
return database_1.prisma.trackingSession.update({
|
|
where: { id: sessionId },
|
|
data: { isActive: false, endedAt: new Date() },
|
|
});
|
|
}
|
|
/** Submit a batch of GPS points for a tracking session. */
|
|
async submitPoints(sessionId, userId, data) {
|
|
const session = await database_1.prisma.trackingSession.findFirst({
|
|
where: { id: sessionId, userId, isActive: true },
|
|
});
|
|
if (!session) {
|
|
throw Object.assign(new Error('Active tracking session not found'), { statusCode: 404 });
|
|
}
|
|
const { points } = data;
|
|
// Batch insert all points
|
|
await database_1.prisma.trackPoint.createMany({
|
|
data: points.map((p) => ({
|
|
trackingSessionId: sessionId,
|
|
latitude: new client_1.Prisma.Decimal(p.latitude),
|
|
longitude: new client_1.Prisma.Decimal(p.longitude),
|
|
accuracy: p.accuracy ?? null,
|
|
recordedAt: new Date(p.recordedAt),
|
|
eventType: p.eventType ?? null,
|
|
})),
|
|
});
|
|
// Calculate incremental distance
|
|
let addedDistance = 0;
|
|
let prevLat = session.lastLatitude ? Number(session.lastLatitude) : null;
|
|
let prevLng = session.lastLongitude ? Number(session.lastLongitude) : null;
|
|
// Sort points by recordedAt for correct distance calculation
|
|
const sorted = [...points].sort((a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime());
|
|
for (const p of sorted) {
|
|
if (prevLat != null && prevLng != null) {
|
|
const d = (0, spatial_1.haversineDistance)(prevLat, prevLng, p.latitude, p.longitude);
|
|
if (d <= MAX_DISTANCE_JUMP_M) {
|
|
addedDistance += d;
|
|
}
|
|
}
|
|
prevLat = p.latitude;
|
|
prevLng = p.longitude;
|
|
}
|
|
const lastPoint = sorted[sorted.length - 1];
|
|
// Update session summary fields
|
|
await database_1.prisma.trackingSession.update({
|
|
where: { id: sessionId },
|
|
data: {
|
|
totalPoints: { increment: points.length },
|
|
totalDistanceM: { increment: addedDistance },
|
|
lastLatitude: new client_1.Prisma.Decimal(lastPoint.latitude),
|
|
lastLongitude: new client_1.Prisma.Decimal(lastPoint.longitude),
|
|
lastRecordedAt: new Date(lastPoint.recordedAt),
|
|
},
|
|
});
|
|
return { inserted: points.length, addedDistanceM: Math.round(addedDistance) };
|
|
}
|
|
/** Get the active tracking session for a user. */
|
|
async getActiveSession(userId) {
|
|
return database_1.prisma.trackingSession.findFirst({
|
|
where: { userId, isActive: true },
|
|
});
|
|
}
|
|
/** Link a canvass session to an existing tracking session. */
|
|
async linkCanvassSession(trackingSessionId, data) {
|
|
return database_1.prisma.trackingSession.update({
|
|
where: { id: trackingSessionId },
|
|
data: { canvassSessionId: data.canvassSessionId },
|
|
});
|
|
}
|
|
/** Get all active volunteers with their recent trail for the admin live map. */
|
|
async getLiveVolunteers(query) {
|
|
const cutoff = new Date(Date.now() - query.staleCutoffMinutes * 60 * 1000);
|
|
const sessions = await database_1.prisma.trackingSession.findMany({
|
|
where: {
|
|
isActive: true,
|
|
lastRecordedAt: { gte: cutoff },
|
|
},
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true } },
|
|
},
|
|
orderBy: { lastRecordedAt: 'desc' },
|
|
});
|
|
// For each session, fetch last 20 track points for the trail polyline
|
|
const results = await Promise.all(sessions.map(async (s) => {
|
|
const recentPoints = await database_1.prisma.trackPoint.findMany({
|
|
where: { trackingSessionId: s.id },
|
|
orderBy: { recordedAt: 'desc' },
|
|
take: 20,
|
|
select: { latitude: true, longitude: true },
|
|
});
|
|
return {
|
|
userId: s.user.id,
|
|
name: s.user.name,
|
|
email: s.user.email,
|
|
latitude: Number(s.lastLatitude),
|
|
longitude: Number(s.lastLongitude),
|
|
lastRecordedAt: s.lastRecordedAt?.toISOString() ?? s.startedAt.toISOString(),
|
|
recentTrail: recentPoints.reverse().map((p) => [Number(p.latitude), Number(p.longitude)]),
|
|
canvassSessionId: s.canvassSessionId,
|
|
trackingSessionId: s.id,
|
|
};
|
|
}));
|
|
return results;
|
|
}
|
|
/** Get the full route for a tracking session, verifying ownership. */
|
|
async getMySessionRoute(trackingSessionId, userId) {
|
|
const session = await database_1.prisma.trackingSession.findFirst({
|
|
where: { id: trackingSessionId, userId },
|
|
});
|
|
if (!session)
|
|
return null;
|
|
return this.getSessionRoute(trackingSessionId);
|
|
}
|
|
/** Get the full route for a tracking session. */
|
|
async getSessionRoute(trackingSessionId) {
|
|
const points = await database_1.prisma.trackPoint.findMany({
|
|
where: { trackingSessionId },
|
|
orderBy: { recordedAt: 'asc' },
|
|
select: {
|
|
latitude: true,
|
|
longitude: true,
|
|
accuracy: true,
|
|
recordedAt: true,
|
|
eventType: true,
|
|
},
|
|
});
|
|
return {
|
|
coordinates: points.map((p) => [Number(p.latitude), Number(p.longitude)]),
|
|
events: points
|
|
.filter((p) => p.eventType != null)
|
|
.map((p) => ({
|
|
latitude: Number(p.latitude),
|
|
longitude: Number(p.longitude),
|
|
eventType: p.eventType,
|
|
recordedAt: p.recordedAt.toISOString(),
|
|
})),
|
|
totalPoints: points.length,
|
|
};
|
|
}
|
|
/** Paginated list of historical tracking sessions. */
|
|
async getHistoricalSessions(query) {
|
|
const where = {};
|
|
if (query.userId)
|
|
where.userId = query.userId;
|
|
if (query.from || query.to) {
|
|
where.startedAt = {};
|
|
if (query.from)
|
|
where.startedAt.gte = new Date(query.from);
|
|
if (query.to)
|
|
where.startedAt.lte = new Date(query.to);
|
|
}
|
|
const [sessions, total] = await Promise.all([
|
|
database_1.prisma.trackingSession.findMany({
|
|
where,
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true } },
|
|
},
|
|
orderBy: { startedAt: 'desc' },
|
|
skip: (query.page - 1) * query.limit,
|
|
take: query.limit,
|
|
}),
|
|
database_1.prisma.trackingSession.count({ where }),
|
|
]);
|
|
return {
|
|
sessions: sessions.map((s) => ({
|
|
id: s.id,
|
|
userId: s.user.id,
|
|
userName: s.user.name,
|
|
userEmail: s.user.email,
|
|
startedAt: s.startedAt.toISOString(),
|
|
endedAt: s.endedAt?.toISOString() ?? null,
|
|
totalPoints: s.totalPoints,
|
|
totalDistanceM: s.totalDistanceM,
|
|
canvassSessionId: s.canvassSessionId,
|
|
isActive: s.isActive,
|
|
})),
|
|
pagination: {
|
|
page: query.page,
|
|
limit: query.limit,
|
|
total,
|
|
totalPages: Math.ceil(total / query.limit),
|
|
},
|
|
};
|
|
}
|
|
/** Delete old track points and empty sessions. */
|
|
async cleanupOldData(retentionDays) {
|
|
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
|
|
const { count: deletedPoints } = await database_1.prisma.trackPoint.deleteMany({
|
|
where: { recordedAt: { lt: cutoff } },
|
|
});
|
|
// Delete sessions with zero remaining points that are inactive
|
|
const emptySessions = await database_1.prisma.trackingSession.findMany({
|
|
where: {
|
|
isActive: false,
|
|
trackPoints: { none: {} },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
if (emptySessions.length > 0) {
|
|
await database_1.prisma.trackingSession.deleteMany({
|
|
where: { id: { in: emptySessions.map((s) => s.id) } },
|
|
});
|
|
}
|
|
if (deletedPoints > 0 || emptySessions.length > 0) {
|
|
logger_1.logger.info(`Tracking cleanup: deleted ${deletedPoints} points, ${emptySessions.length} empty sessions`);
|
|
}
|
|
}
|
|
/** Mark stale tracking sessions as inactive. */
|
|
async closeStaleTrackingSessions(minutes) {
|
|
const cutoff = new Date(Date.now() - minutes * 60 * 1000);
|
|
const { count } = await database_1.prisma.trackingSession.updateMany({
|
|
where: {
|
|
isActive: true,
|
|
OR: [
|
|
{ lastRecordedAt: { lt: cutoff } },
|
|
{ lastRecordedAt: null, startedAt: { lt: cutoff } },
|
|
],
|
|
},
|
|
data: { isActive: false, endedAt: new Date() },
|
|
});
|
|
if (count > 0) {
|
|
logger_1.logger.info(`Closed ${count} stale tracking sessions (no data for ${minutes}min)`);
|
|
}
|
|
}
|
|
}
|
|
exports.trackingService = new TrackingService();
|
|
//# sourceMappingURL=tracking.service.js.map
|