Upgrade system finished
This commit is contained in:
@@ -1 +1 @@
|
||||
{"version":3,"file":"canvass.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.routes.ts"],"names":[],"mappings":"AAyBA,QAAA,MAAM,eAAe,4CAAW,CAAC;AAgQjC,QAAA,MAAM,WAAW,4CAAW,CAAC;AAsF7B,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,WAAW,IAAI,kBAAkB,EAAE,CAAC"}
|
||||
{"version":3,"file":"canvass.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.routes.ts"],"names":[],"mappings":"AAyBA,QAAA,MAAM,eAAe,4CAAW,CAAC;AAgQjC,QAAA,MAAM,WAAW,4CAAW,CAAC;AAoG7B,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,WAAW,IAAI,kBAAkB,EAAE,CAAC"}
|
||||
20
api/dist/modules/map/canvass/canvass.routes.js
vendored
20
api/dist/modules/map/canvass/canvass.routes.js
vendored
@@ -12,7 +12,7 @@ const validate_1 = require("../../../middleware/validate");
|
||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||
const rate_limit_1 = require("../../../middleware/rate-limit");
|
||||
const MAP_ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN];
|
||||
const roles_1 = require("../../../utils/roles");
|
||||
// ─── Volunteer Router ────────────────────────────────────────────────
|
||||
const volunteerRouter = (0, express_1.Router)();
|
||||
exports.canvassVolunteerRouter = volunteerRouter;
|
||||
@@ -138,17 +138,17 @@ volunteerRouter.put('/locations/:id', (0, validate_1.validate)(canvass_schemas_1
|
||||
// POST /api/map/canvass/locations — create a new location (role-gated fields)
|
||||
volunteerRouter.post('/locations', (0, validate_1.validate)(canvass_schemas_1.volunteerCreateLocationSchema), async (req, res, next) => {
|
||||
try {
|
||||
const role = req.user.role;
|
||||
const data = { ...req.body };
|
||||
// Strip fields based on role
|
||||
const isAdmin = role === client_1.UserRole.SUPER_ADMIN || role === client_1.UserRole.MAP_ADMIN;
|
||||
const userRoles = req.user.roles || [req.user.role];
|
||||
const isAdmin = userRoles.some((r) => r === client_1.UserRole.SUPER_ADMIN || r === client_1.UserRole.MAP_ADMIN);
|
||||
if (!isAdmin) {
|
||||
delete data.firstName;
|
||||
delete data.lastName;
|
||||
delete data.email;
|
||||
delete data.phone;
|
||||
}
|
||||
if (role === client_1.UserRole.TEMP) {
|
||||
if (userRoles.length === 1 && userRoles[0] === client_1.UserRole.TEMP) {
|
||||
delete data.supportLevel;
|
||||
delete data.sign;
|
||||
delete data.signSize;
|
||||
@@ -210,7 +210,7 @@ volunteerRouter.post('/visits/bulk', rate_limit_1.canvassBulkVisitRateLimit, //
|
||||
const adminRouter = (0, express_1.Router)();
|
||||
exports.canvassAdminRouter = adminRouter;
|
||||
adminRouter.use(auth_middleware_1.authenticate);
|
||||
adminRouter.use((0, rbac_middleware_1.requireRole)(...MAP_ADMIN_ROLES));
|
||||
adminRouter.use((0, rbac_middleware_1.requireRole)(...roles_1.MAP_ROLES));
|
||||
// GET /api/map/canvass/stats
|
||||
adminRouter.get('/stats', async (_req, res, next) => {
|
||||
try {
|
||||
@@ -273,4 +273,14 @@ adminRouter.get('/visits', (0, validate_1.validate)(canvass_schemas_1.adminVisit
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/map/canvass/trends
|
||||
adminRouter.get('/trends', (0, validate_1.validate)(canvass_schemas_1.outcomeTrendsQuerySchema, 'query'), async (req, res, next) => {
|
||||
try {
|
||||
const result = await canvass_service_1.canvassService.getOutcomeTrends(req.query);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=canvass.routes.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -30,20 +30,20 @@ export declare const recordVisitSchema: z.ZodObject<{
|
||||
updateLocation: boolean;
|
||||
durationSeconds?: number | undefined;
|
||||
sessionId?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
supportLevel?: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4" | undefined;
|
||||
signSize?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
}, {
|
||||
outcome: "NOT_HOME" | "REFUSED" | "MOVED" | "ALREADY_VOTED" | "SPOKE_WITH" | "LEFT_LITERATURE" | "COME_BACK_LATER";
|
||||
addressId: string;
|
||||
durationSeconds?: number | undefined;
|
||||
sessionId?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
supportLevel?: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4" | undefined;
|
||||
signSize?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
signRequested?: boolean | undefined;
|
||||
signSize?: string | undefined;
|
||||
updateLocation?: boolean | undefined;
|
||||
}>;
|
||||
export declare const bulkRecordVisitSchema: z.ZodObject<{
|
||||
@@ -56,14 +56,14 @@ export declare const bulkRecordVisitSchema: z.ZodObject<{
|
||||
outcome: "NOT_HOME" | "REFUSED" | "MOVED";
|
||||
locationId: string;
|
||||
sessionId?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
}, {
|
||||
outcome: "NOT_HOME" | "REFUSED" | "MOVED";
|
||||
locationId: string;
|
||||
sessionId?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
}>;
|
||||
export declare const startSessionSchema: z.ZodObject<{
|
||||
cutId: z.ZodString;
|
||||
@@ -156,17 +156,17 @@ export declare const adminVisitsSchema: z.ZodObject<{
|
||||
sortOrder: "asc" | "desc";
|
||||
userId?: string | undefined;
|
||||
outcome?: "NOT_HOME" | "REFUSED" | "MOVED" | "ALREADY_VOTED" | "SPOKE_WITH" | "LEFT_LITERATURE" | "COME_BACK_LATER" | undefined;
|
||||
shiftId?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
}, {
|
||||
limit?: number | undefined;
|
||||
userId?: string | undefined;
|
||||
page?: number | undefined;
|
||||
outcome?: "NOT_HOME" | "REFUSED" | "MOVED" | "ALREADY_VOTED" | "SPOKE_WITH" | "LEFT_LITERATURE" | "COME_BACK_LATER" | undefined;
|
||||
shiftId?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
sortBy?: "outcome" | "visitedAt" | undefined;
|
||||
sortOrder?: "asc" | "desc" | undefined;
|
||||
outcome?: "NOT_HOME" | "REFUSED" | "MOVED" | "ALREADY_VOTED" | "SPOKE_WITH" | "LEFT_LITERATURE" | "COME_BACK_LATER" | undefined;
|
||||
cutId?: string | undefined;
|
||||
shiftId?: string | undefined;
|
||||
}>;
|
||||
export declare const volunteerUpdateLocationSchema: z.ZodObject<{
|
||||
supportLevel: z.ZodOptional<z.ZodNullable<z.ZodNativeEnum<{
|
||||
@@ -187,25 +187,25 @@ export declare const volunteerUpdateLocationSchema: z.ZodObject<{
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
email?: string | null | undefined;
|
||||
phone?: string | null | undefined;
|
||||
address?: string | null | undefined;
|
||||
unitNumber?: string | null | undefined;
|
||||
notes?: string | null | undefined;
|
||||
firstName?: string | null | undefined;
|
||||
lastName?: string | null | undefined;
|
||||
supportLevel?: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4" | null | undefined;
|
||||
address?: string | null | undefined;
|
||||
unitNumber?: string | null | undefined;
|
||||
sign?: boolean | undefined;
|
||||
signSize?: string | null | undefined;
|
||||
notes?: string | null | undefined;
|
||||
}, {
|
||||
email?: string | null | undefined;
|
||||
phone?: string | null | undefined;
|
||||
address?: string | null | undefined;
|
||||
unitNumber?: string | null | undefined;
|
||||
notes?: string | null | undefined;
|
||||
firstName?: string | null | undefined;
|
||||
lastName?: string | null | undefined;
|
||||
supportLevel?: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4" | null | undefined;
|
||||
address?: string | null | undefined;
|
||||
unitNumber?: string | null | undefined;
|
||||
sign?: boolean | undefined;
|
||||
signSize?: string | null | undefined;
|
||||
notes?: string | null | undefined;
|
||||
}>;
|
||||
export declare const volunteerCreateLocationSchema: z.ZodObject<{
|
||||
address: z.ZodString;
|
||||
@@ -232,25 +232,25 @@ export declare const volunteerCreateLocationSchema: z.ZodObject<{
|
||||
sign: boolean;
|
||||
email?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
unitNumber?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
firstName?: string | undefined;
|
||||
lastName?: string | undefined;
|
||||
supportLevel?: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4" | undefined;
|
||||
unitNumber?: string | undefined;
|
||||
signSize?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
}, {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
email?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
unitNumber?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
firstName?: string | undefined;
|
||||
lastName?: string | undefined;
|
||||
supportLevel?: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4" | undefined;
|
||||
unitNumber?: string | undefined;
|
||||
sign?: boolean | undefined;
|
||||
signSize?: string | undefined;
|
||||
notes?: string | undefined;
|
||||
}>;
|
||||
export type RecordVisitInput = z.infer<typeof recordVisitSchema>;
|
||||
export type BulkRecordVisitInput = z.infer<typeof bulkRecordVisitSchema>;
|
||||
@@ -259,6 +259,20 @@ export type WalkingRouteInput = z.infer<typeof walkingRouteSchema>;
|
||||
export type ListMyVisitsInput = z.infer<typeof listMyVisitsSchema>;
|
||||
export type AdminActivityInput = z.infer<typeof adminActivitySchema>;
|
||||
export type AdminVisitsInput = z.infer<typeof adminVisitsSchema>;
|
||||
export declare const outcomeTrendsQuerySchema: z.ZodObject<{
|
||||
granularity: z.ZodDefault<z.ZodEnum<["day", "week"]>>;
|
||||
dateFrom: z.ZodOptional<z.ZodString>;
|
||||
dateTo: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
granularity: "week" | "day";
|
||||
dateFrom?: string | undefined;
|
||||
dateTo?: string | undefined;
|
||||
}, {
|
||||
dateFrom?: string | undefined;
|
||||
dateTo?: string | undefined;
|
||||
granularity?: "week" | "day" | undefined;
|
||||
}>;
|
||||
export type VolunteerUpdateLocationInput = z.infer<typeof volunteerUpdateLocationSchema>;
|
||||
export type VolunteerCreateLocationInput = z.infer<typeof volunteerCreateLocationSchema>;
|
||||
export type OutcomeTrendsQueryInput = z.infer<typeof outcomeTrendsQuerySchema>;
|
||||
//# sourceMappingURL=canvass.schemas.d.ts.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"canvass.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAW5B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;EAMhC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAK7B,CAAC;AAEH,eAAO,MAAM,gBAAgB,gDAE3B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;EAI7B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;EAG7B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;EAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS5B,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYxC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAexC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACzE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACnE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACrE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AACzF,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
|
||||
{"version":3,"file":"canvass.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAW5B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;EAMhC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAK7B,CAAC;AAEH,eAAO,MAAM,gBAAgB,gDAE3B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;EAI7B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;EAG7B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;EAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS5B,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYxC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAexC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACzE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACnE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACrE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;EAInC,CAAC;AAEH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AACzF,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AACzF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.volunteerCreateLocationSchema = exports.volunteerUpdateLocationSchema = exports.adminVisitsSchema = exports.adminActivitySchema = exports.listMyVisitsSchema = exports.walkingRouteSchema = exports.endSessionSchema = exports.startSessionSchema = exports.bulkRecordVisitSchema = exports.recordVisitSchema = void 0;
|
||||
exports.outcomeTrendsQuerySchema = exports.volunteerCreateLocationSchema = exports.volunteerUpdateLocationSchema = exports.adminVisitsSchema = exports.adminActivitySchema = exports.listMyVisitsSchema = exports.walkingRouteSchema = exports.endSessionSchema = exports.startSessionSchema = exports.bulkRecordVisitSchema = exports.recordVisitSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const client_1 = require("@prisma/client");
|
||||
exports.recordVisitSchema = zod_1.z.object({
|
||||
@@ -86,4 +86,9 @@ exports.volunteerCreateLocationSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email().optional().or(zod_1.z.literal('')),
|
||||
phone: zod_1.z.string().optional(),
|
||||
});
|
||||
exports.outcomeTrendsQuerySchema = zod_1.z.object({
|
||||
granularity: zod_1.z.enum(['day', 'week']).default('day'),
|
||||
dateFrom: zod_1.z.string().optional(),
|
||||
dateTo: zod_1.z.string().optional(),
|
||||
});
|
||||
//# sourceMappingURL=canvass.schemas.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import { Prisma, UserRole } from '@prisma/client';
|
||||
import type { RecordVisitInput, BulkRecordVisitInput, StartSessionInput, WalkingRouteInput, ListMyVisitsInput, AdminActivityInput, AdminVisitsInput, VolunteerUpdateLocationInput } from './canvass.schemas';
|
||||
import type { RecordVisitInput, BulkRecordVisitInput, StartSessionInput, WalkingRouteInput, ListMyVisitsInput, AdminActivityInput, AdminVisitsInput, VolunteerUpdateLocationInput, OutcomeTrendsQueryInput } from './canvass.schemas';
|
||||
export declare const canvassService: {
|
||||
getMyAssignments(userId: string): Promise<{
|
||||
shiftId: string;
|
||||
@@ -32,12 +32,12 @@ export declare const canvassService: {
|
||||
durationSeconds: number | null;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
notes: string | null;
|
||||
outcome: import(".prisma/client").$Enums.VisitOutcome;
|
||||
shiftId: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
signRequested: boolean;
|
||||
signSize: string | null;
|
||||
visitedAt: Date;
|
||||
addressId: string;
|
||||
})[];
|
||||
@@ -61,12 +61,12 @@ export declare const canvassService: {
|
||||
status: import(".prisma/client").$Enums.CanvassSessionStatus;
|
||||
id: string;
|
||||
userId: string;
|
||||
cutId: string;
|
||||
shiftId: string | null;
|
||||
startedAt: Date;
|
||||
endedAt: Date | null;
|
||||
startLatitude: Prisma.Decimal | null;
|
||||
startLongitude: Prisma.Decimal | null;
|
||||
cutId: string;
|
||||
}) | null>;
|
||||
startSession(userId: string, data: StartSessionInput): Promise<{
|
||||
shift: {
|
||||
@@ -81,23 +81,23 @@ export declare const canvassService: {
|
||||
status: import(".prisma/client").$Enums.CanvassSessionStatus;
|
||||
id: string;
|
||||
userId: string;
|
||||
cutId: string;
|
||||
shiftId: string | null;
|
||||
startedAt: Date;
|
||||
endedAt: Date | null;
|
||||
startLatitude: Prisma.Decimal | null;
|
||||
startLongitude: Prisma.Decimal | null;
|
||||
cutId: string;
|
||||
}>;
|
||||
endSession(sessionId: string, userId: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.CanvassSessionStatus;
|
||||
id: string;
|
||||
userId: string;
|
||||
cutId: string;
|
||||
shiftId: string | null;
|
||||
startedAt: Date;
|
||||
endedAt: Date | null;
|
||||
startLatitude: Prisma.Decimal | null;
|
||||
startLongitude: Prisma.Decimal | null;
|
||||
cutId: string;
|
||||
}>;
|
||||
getCutLocationsForCanvass(cutId: string, userId: string, bounds?: {
|
||||
minLat: number;
|
||||
@@ -121,13 +121,13 @@ export declare const canvassService: {
|
||||
id: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
unitNumber: string | null;
|
||||
notes: string | null;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
unitNumber: string | null;
|
||||
sign: boolean;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
}[]>;
|
||||
getAllLocationsForCanvass(userId: string, bounds?: {
|
||||
minLat: number;
|
||||
@@ -151,13 +151,13 @@ export declare const canvassService: {
|
||||
id: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
unitNumber: string | null;
|
||||
notes: string | null;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
unitNumber: string | null;
|
||||
sign: boolean;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
}[]>;
|
||||
updateAddressAsVolunteer(addressId: string, userId: string, role: UserRole, data: VolunteerUpdateLocationInput): Promise<{
|
||||
id: string;
|
||||
@@ -165,16 +165,16 @@ export declare const canvassService: {
|
||||
phone: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
createdByUserId: string | null;
|
||||
updatedByUserId: string | null;
|
||||
unitNumber: string | null;
|
||||
addrGuid: string | null;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
unitNumber: string | null;
|
||||
addrGuid: string | null;
|
||||
sign: boolean;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
locationId: string;
|
||||
}>;
|
||||
getWalkingRoute(cutId: string, userId: string, filters: WalkingRouteInput): Promise<import("./canvass-route.service").RouteResult>;
|
||||
@@ -191,12 +191,12 @@ export declare const canvassService: {
|
||||
durationSeconds: number | null;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
notes: string | null;
|
||||
outcome: import(".prisma/client").$Enums.VisitOutcome;
|
||||
shiftId: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
signRequested: boolean;
|
||||
signSize: string | null;
|
||||
visitedAt: Date;
|
||||
addressId: string;
|
||||
}>;
|
||||
@@ -207,12 +207,12 @@ export declare const canvassService: {
|
||||
durationSeconds: number | null;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
notes: string | null;
|
||||
outcome: import(".prisma/client").$Enums.VisitOutcome;
|
||||
shiftId: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
signRequested: boolean;
|
||||
signSize: string | null;
|
||||
visitedAt: Date;
|
||||
addressId: string;
|
||||
}[];
|
||||
@@ -252,12 +252,12 @@ export declare const canvassService: {
|
||||
durationSeconds: number | null;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
notes: string | null;
|
||||
outcome: import(".prisma/client").$Enums.VisitOutcome;
|
||||
shiftId: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
signRequested: boolean;
|
||||
signSize: string | null;
|
||||
visitedAt: Date;
|
||||
addressId: string;
|
||||
})[];
|
||||
@@ -301,12 +301,12 @@ export declare const canvassService: {
|
||||
durationSeconds: number | null;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
notes: string | null;
|
||||
outcome: import(".prisma/client").$Enums.VisitOutcome;
|
||||
shiftId: string | null;
|
||||
supportLevel: import(".prisma/client").$Enums.SupportLevel | null;
|
||||
signSize: string | null;
|
||||
notes: string | null;
|
||||
signRequested: boolean;
|
||||
signSize: string | null;
|
||||
visitedAt: Date;
|
||||
addressId: string;
|
||||
})[];
|
||||
@@ -317,6 +317,15 @@ export declare const canvassService: {
|
||||
totalPages: number;
|
||||
};
|
||||
}>;
|
||||
getOutcomeTrends(filters: OutcomeTrendsQueryInput): Promise<{
|
||||
granularity: "week" | "day";
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
series: {
|
||||
date: string;
|
||||
}[];
|
||||
totals: Record<string, number>;
|
||||
}>;
|
||||
recalculateCutCompletion(cutId: string): Promise<void>;
|
||||
closeAbandonedSessions(): Promise<number>;
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"canvass.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoD,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AASpG,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,4BAA4B,EAC7B,MAAM,mBAAmB,CAAC;AAwE3B,eAAO,MAAM,cAAc;6BAGM,MAAM;;;;;;;;;;;uBAgCZ,MAAM;;;;;;wBAuBL,MAAM,WAAW,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BA6B7B,MAAM;;;;;;;;;;;;;;;;;;;;yBAUV,MAAM,QAAQ,iBAAiB;;;;;;;;;;;;;;;;;;;;0BAgC9B,MAAM,UAAU,MAAM;;;;;;;;;;;qCAwBzC,MAAM,UACL,MAAM,WACL;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,UACnE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;sCAoGN,MAAM,WACL;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,UACnE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;wCAuFH,MAAM,UACT,MAAM,QACR,QAAQ,QACR,4BAA4B;;;;;;;;;;;;;;;;;;2BA+BP,MAAM,UAAU,MAAM,WAAW,iBAAiB;wBA2BrD,MAAM,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;;;;4BA0D1B,MAAM,QAAQ,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;uBAqHvC,MAAM;;;;;;;;;8BAuDC,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA0FlB,MAAM;;;;;;4BAIR,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCA4DR,MAAM;;CA8D7C,CAAC"}
|
||||
{"version":3,"file":"canvass.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoD,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAgBpG,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,4BAA4B,EAC5B,uBAAuB,EACxB,MAAM,mBAAmB,CAAC;AAwE3B,eAAO,MAAM,cAAc;6BAGM,MAAM;;;;;;;;;;;uBAgCZ,MAAM;;;;;;wBAuBL,MAAM,WAAW,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BA6B7B,MAAM;;;;;;;;;;;;;;;;;;;;yBAUV,MAAM,QAAQ,iBAAiB;;;;;;;;;;;;;;;;;;;;0BAgC9B,MAAM,UAAU,MAAM;;;;;;;;;;;qCA4GzC,MAAM,UACL,MAAM,WACL;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,UACnE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;sCAoGN,MAAM,WACL;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,UACnE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;wCAuFH,MAAM,UACT,MAAM,QACR,QAAQ,QACR,4BAA4B;;;;;;;;;;;;;;;;;;2BA+BP,MAAM,UAAU,MAAM,WAAW,iBAAiB;wBA2BrD,MAAM,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;;;;4BAuH1B,MAAM,QAAQ,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;uBAqHvC,MAAM;;;;;;;;;8BAuDC,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA0FlB,MAAM;;;;;;4BAIR,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA0Dd,uBAAuB;;;;;;;;;oCAoDjB,MAAM;;CA8D7C,CAAC"}
|
||||
187
api/dist/modules/map/canvass/canvass.service.js
vendored
187
api/dist/modules/map/canvass/canvass.service.js
vendored
@@ -8,8 +8,15 @@ const error_handler_1 = require("../../../middleware/error-handler");
|
||||
const logger_1 = require("../../../utils/logger");
|
||||
const metrics_1 = require("../../../utils/metrics");
|
||||
const spatial_1 = require("../../../utils/spatial");
|
||||
const crm_activity_1 = require("../../../utils/crm-activity");
|
||||
const canvass_route_service_1 = require("./canvass-route.service");
|
||||
const metrics_2 = require("../../../utils/metrics");
|
||||
const notification_queue_service_1 = require("../../../services/notification-queue.service");
|
||||
const notification_helper_1 = require("../../../services/notification.helper");
|
||||
const env_1 = require("../../../config/env");
|
||||
const rocketchat_webhook_service_1 = require("../../../services/rocketchat-webhook.service");
|
||||
const listmonk_event_sync_service_1 = require("../../../services/listmonk-event-sync.service");
|
||||
const achievements_service_1 = require("../../social/achievements.service");
|
||||
const ADDRESS_SELECT = {
|
||||
id: true,
|
||||
unitNumber: true,
|
||||
@@ -201,6 +208,87 @@ exports.canvassService = {
|
||||
});
|
||||
// Recalculate cut completion percentage
|
||||
await this.recalculateCutCompletion(session.cutId);
|
||||
// Notify Rocket.Chat
|
||||
try {
|
||||
const [rcUser, rcCut, rcVisitCount] = await Promise.all([
|
||||
database_1.prisma.user.findUnique({ where: { id: userId }, select: { name: true, email: true } }),
|
||||
database_1.prisma.cut.findUnique({ where: { id: session.cutId }, select: { name: true } }),
|
||||
database_1.prisma.canvassVisit.count({ where: { sessionId } }),
|
||||
]);
|
||||
rocketchat_webhook_service_1.rocketchatWebhookService.onCanvassSessionCompleted({
|
||||
userName: rcUser?.name || rcUser?.email || 'Unknown',
|
||||
visitCount: rcVisitCount,
|
||||
cutName: rcCut?.name || undefined,
|
||||
}).catch(() => { });
|
||||
}
|
||||
catch { /* non-critical */ }
|
||||
// Notification: volunteer session summary
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerSessionSummary')) {
|
||||
const [user, cut, visitCount, outcomeGroups, trackingSession] = await Promise.all([
|
||||
database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } }),
|
||||
database_1.prisma.cut.findUnique({ where: { id: session.cutId }, select: { name: true } }),
|
||||
database_1.prisma.canvassVisit.count({ where: { sessionId } }),
|
||||
database_1.prisma.canvassVisit.groupBy({ by: ['outcome'], where: { sessionId }, _count: true }),
|
||||
database_1.prisma.trackingSession.findFirst({
|
||||
where: { userId, canvassSessionId: sessionId },
|
||||
select: { totalDistanceM: true },
|
||||
}),
|
||||
]);
|
||||
if (user && visitCount > 0) {
|
||||
const durationMs = updated.endedAt
|
||||
? updated.endedAt.getTime() - session.startedAt.getTime()
|
||||
: 0;
|
||||
const durationMinutes = Math.round(durationMs / 60000);
|
||||
const distanceKm = trackingSession?.totalDistanceM
|
||||
? Number(trackingSession.totalDistanceM) / 1000
|
||||
: 0;
|
||||
const outcomeBreakdown = {};
|
||||
for (const row of outcomeGroups) {
|
||||
outcomeBreakdown[row.outcome] = row._count;
|
||||
}
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'volunteer-session-summary',
|
||||
volunteerEmail: user.email,
|
||||
volunteerName: user.name || user.email,
|
||||
cutName: cut?.name || 'Unknown area',
|
||||
sessionDate: session.startedAt.toLocaleDateString('en-CA', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||||
}),
|
||||
visitCount,
|
||||
durationMinutes,
|
||||
distanceKm,
|
||||
outcomeBreakdown,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue session summary notification:', err);
|
||||
}
|
||||
// Listmonk event sync — add canvasser to subscribers
|
||||
try {
|
||||
const [syncUser, syncCut, syncVisitCount, syncOutcomes] = await Promise.all([
|
||||
database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } }),
|
||||
database_1.prisma.cut.findUnique({ where: { id: session.cutId }, select: { name: true } }),
|
||||
database_1.prisma.canvassVisit.count({ where: { sessionId } }),
|
||||
database_1.prisma.canvassVisit.groupBy({ by: ['outcome'], where: { sessionId }, _count: true }),
|
||||
]);
|
||||
if (syncUser) {
|
||||
const outcomes = {};
|
||||
for (const row of syncOutcomes) {
|
||||
outcomes[row.outcome] = row._count;
|
||||
}
|
||||
listmonk_event_sync_service_1.listmonkEventSyncService.onCanvassSessionCompleted({
|
||||
email: syncUser.email,
|
||||
name: syncUser.name || syncUser.email,
|
||||
cutName: syncCut?.name || 'Unknown',
|
||||
visitCount: syncVisitCount,
|
||||
outcomes,
|
||||
}).catch(() => { });
|
||||
}
|
||||
}
|
||||
catch { /* non-critical */ }
|
||||
return updated;
|
||||
},
|
||||
async getCutLocationsForCanvass(cutId, userId, bounds, limit) {
|
||||
@@ -448,12 +536,68 @@ exports.canvassService = {
|
||||
const prefix = `[${timestamp}] ${data.notes}`;
|
||||
updateData.notes = address.notes ? `${prefix}\n${address.notes}` : prefix;
|
||||
}
|
||||
await database_1.prisma.address.update({
|
||||
const updatedAddress = await database_1.prisma.address.update({
|
||||
where: { id: data.addressId },
|
||||
data: updateData,
|
||||
include: { location: { select: { address: true } } },
|
||||
});
|
||||
// Sync support level change to Listmonk (fire-and-forget)
|
||||
if (updatedAddress.email) {
|
||||
const name = [updatedAddress.firstName, updatedAddress.lastName].filter(Boolean).join(' ');
|
||||
listmonk_event_sync_service_1.listmonkEventSyncService.onAddressUpdated({
|
||||
email: updatedAddress.email,
|
||||
name,
|
||||
supportLevel: updatedAddress.supportLevel,
|
||||
sign: updatedAddress.sign,
|
||||
address: updatedAddress.location.address,
|
||||
}).catch(() => { });
|
||||
}
|
||||
}
|
||||
(0, metrics_2.recordCanvassVisit)(data.outcome);
|
||||
// CRM activity via ContactAddress lookup (fire-and-forget)
|
||||
database_1.prisma.contactAddress.findFirst({
|
||||
where: { addressId: data.addressId },
|
||||
select: { contactId: true },
|
||||
}).then((ca) => {
|
||||
if (ca) {
|
||||
(0, crm_activity_1.recordCrmActivity)({
|
||||
contactId: ca.contactId,
|
||||
activityType: 'CANVASS_VISIT',
|
||||
title: `Canvass visit: ${data.outcome}`,
|
||||
metadata: { addressId: data.addressId, outcome: data.outcome, visitId: visit.id },
|
||||
}).catch(() => { });
|
||||
}
|
||||
}).catch(() => { });
|
||||
// Achievement check (fire-and-forget)
|
||||
achievements_service_1.achievementsService.checkAndUnlock(userId, ['canvass']).catch(() => { });
|
||||
// Notification: sign request alert for admins
|
||||
if (data.signRequested) {
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminSignRequested')) {
|
||||
const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
|
||||
if (adminEmails.length > 0) {
|
||||
const [volunteer, shift] = await Promise.all([
|
||||
database_1.prisma.user.findUnique({ where: { id: userId }, select: { name: true } }),
|
||||
data.shiftId ? database_1.prisma.shift.findUnique({ where: { id: data.shiftId }, select: { title: true } }) : null,
|
||||
]);
|
||||
const addressStr = visit.address?.location?.address || 'Unknown address';
|
||||
const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/canvass/dashboard`;
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'admin-sign-requested',
|
||||
adminEmails,
|
||||
volunteerName: volunteer?.name || 'Unknown',
|
||||
address: addressStr,
|
||||
shiftTitle: shift?.title || 'No shift',
|
||||
signSize: data.signSize || '',
|
||||
adminUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue sign request notification:', err);
|
||||
}
|
||||
}
|
||||
return visit;
|
||||
},
|
||||
async recordBulkVisit(userId, data) {
|
||||
@@ -736,6 +880,47 @@ exports.canvassService = {
|
||||
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
|
||||
};
|
||||
},
|
||||
async getOutcomeTrends(filters) {
|
||||
const { granularity } = filters;
|
||||
const dateTo = filters.dateTo ? new Date(filters.dateTo) : new Date();
|
||||
const dateFrom = filters.dateFrom
|
||||
? new Date(filters.dateFrom)
|
||||
: new Date(dateTo.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
// Ensure dateTo covers end of day
|
||||
const dateToEnd = new Date(dateTo);
|
||||
dateToEnd.setHours(23, 59, 59, 999);
|
||||
const rows = await database_1.prisma.$queryRaw `
|
||||
SELECT DATE_TRUNC(${granularity}, "visitedAt") as period,
|
||||
outcome::text as outcome,
|
||||
COUNT(*)::int as count
|
||||
FROM canvass_visits
|
||||
WHERE "visitedAt" >= ${dateFrom} AND "visitedAt" <= ${dateToEnd}
|
||||
GROUP BY period, outcome
|
||||
ORDER BY period ASC
|
||||
`;
|
||||
// Pivot rows into series: [{ date, NOT_HOME: n, SPOKE_WITH: n, ... }]
|
||||
const pivotMap = new Map();
|
||||
const totals = {};
|
||||
for (const row of rows) {
|
||||
const dateStr = row.period.toISOString().split('T')[0];
|
||||
if (!pivotMap.has(dateStr)) {
|
||||
pivotMap.set(dateStr, {});
|
||||
}
|
||||
pivotMap.get(dateStr)[row.outcome] = row.count;
|
||||
totals[row.outcome] = (totals[row.outcome] || 0) + row.count;
|
||||
}
|
||||
const series = Array.from(pivotMap.entries()).map(([date, outcomes]) => ({
|
||||
date,
|
||||
...outcomes,
|
||||
}));
|
||||
return {
|
||||
granularity,
|
||||
dateFrom: dateFrom.toISOString().split('T')[0],
|
||||
dateTo: dateTo.toISOString().split('T')[0],
|
||||
series,
|
||||
totals,
|
||||
};
|
||||
},
|
||||
// ─── Helpers ───────────────────────────────────────────────────────
|
||||
async recalculateCutCompletion(cutId) {
|
||||
const cut = await database_1.prisma.cut.findUnique({ where: { id: cutId } });
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user