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