Upgrade system finished
This commit is contained in:
@@ -1 +1 @@
|
||||
{"version":3,"file":"shift-series.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2DxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"shift-series.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqExB,eAAe,MAAM,CAAC"}
|
||||
@@ -4,12 +4,12 @@ const express_1 = require("express");
|
||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||
const validate_1 = require("../../../middleware/validate");
|
||||
const client_1 = require("@prisma/client");
|
||||
const shift_series_service_1 = require("./shift-series.service");
|
||||
const shift_series_schemas_1 = require("./shift-series.schemas");
|
||||
const roles_1 = require("../../../utils/roles");
|
||||
const router = (0, express_1.Router)();
|
||||
// All routes require admin role
|
||||
router.use(auth_middleware_1.authenticate, (0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN));
|
||||
router.use(auth_middleware_1.authenticate, (0, rbac_middleware_1.requireRole)(...roles_1.SCHEDULING_ROLES));
|
||||
// Create series
|
||||
router.post('/', (0, validate_1.validate)(shift_series_schemas_1.createShiftSeriesSchema), async (req, res, next) => {
|
||||
try {
|
||||
@@ -20,6 +20,16 @@ router.post('/', (0, validate_1.validate)(shift_series_schemas_1.createShiftSeri
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// Get series shift count
|
||||
router.get('/:id/count', async (req, res, next) => {
|
||||
try {
|
||||
const count = await shift_series_service_1.ShiftSeriesService.getShiftCount(req.params.id);
|
||||
res.json({ count });
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// Get series
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"shift-series.routes.js","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.routes.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AACjC,yEAAmE;AACnE,yEAAkE;AAClE,2DAAwD;AACxD,2CAA0C;AAC1C,iEAA4D;AAC5D,iEAA0F;AAE1F,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,8BAAY,EAAE,IAAA,6BAAW,EAAC,iBAAQ,CAAC,WAAW,EAAE,iBAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AAEhF,gBAAgB;AAChB,MAAM,CAAC,IAAI,CACT,GAAG,EACH,IAAA,mBAAQ,EAAC,8CAAuB,CAAC,EACjC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,yCAAkB,CAAC,YAAY,CAClD,GAAG,CAAC,IAAI,EACR,GAAG,CAAC,IAAI,EAAE,EAAE,CACb,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CACF,CAAC;AAEF,aAAa;AACb,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,yCAAkB,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAY,CAAC,CAAC;QAC3E,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,gBAAgB;AAChB,MAAM,CAAC,GAAG,CACR,MAAM,EACN,IAAA,mBAAQ,EAAC,8CAAuB,CAAC,EACjC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,yCAAkB,CAAC,YAAY,CAClD,GAAG,CAAC,MAAM,CAAC,EAAY,EACvB,GAAG,CAAC,IAAI,CACT,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CACF,CAAC;AAEF,gBAAgB;AAChB,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAC7C,IAAI,CAAC;QACH,MAAM,yCAAkB,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,EAAY,CAAC,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"shift-series.routes.js","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.routes.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AACjC,yEAAmE;AACnE,yEAAkE;AAClE,2DAAwD;AACxD,iEAA4D;AAC5D,iEAA0F;AAC1F,gDAAwD;AAExD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,8BAAY,EAAE,IAAA,6BAAW,EAAC,GAAG,wBAAgB,CAAC,CAAC,CAAC;AAE3D,gBAAgB;AAChB,MAAM,CAAC,IAAI,CACT,GAAG,EACH,IAAA,mBAAQ,EAAC,8CAAuB,CAAC,EACjC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,yCAAkB,CAAC,YAAY,CAClD,GAAG,CAAC,IAAI,EACR,GAAG,CAAC,IAAI,EAAE,EAAE,CACb,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CACF,CAAC;AAEF,yBAAyB;AACzB,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,yCAAkB,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAY,CAAC,CAAC;QAC9E,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,aAAa;AACb,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,yCAAkB,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAY,CAAC,CAAC;QAC3E,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,gBAAgB;AAChB,MAAM,CAAC,GAAG,CACR,MAAM,EACN,IAAA,mBAAQ,EAAC,8CAAuB,CAAC,EACjC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACvB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,yCAAkB,CAAC,YAAY,CAClD,GAAG,CAAC,MAAM,CAAC,EAAY,EACvB,GAAG,CAAC,IAAI,CACT,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CACF,CAAC;AAEF,gBAAgB;AAChB,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAC7C,IAAI,CAAC;QACH,MAAM,yCAAkB,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,EAAY,CAAC,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}
|
||||
@@ -25,8 +25,8 @@ export declare const createShiftSeriesSchema: z.ZodEffects<z.ZodObject<{
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
description?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
}, {
|
||||
@@ -37,8 +37,8 @@ export declare const createShiftSeriesSchema: z.ZodEffects<z.ZodObject<{
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
description?: string | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
@@ -51,8 +51,8 @@ export declare const createShiftSeriesSchema: z.ZodEffects<z.ZodObject<{
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
description?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
}, {
|
||||
@@ -63,8 +63,8 @@ export declare const createShiftSeriesSchema: z.ZodEffects<z.ZodObject<{
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
description?: string | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
|
||||
@@ -14,12 +14,12 @@ export declare class ShiftSeriesService {
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
@@ -28,43 +28,26 @@ export declare class ShiftSeriesService {
|
||||
};
|
||||
generatedShiftsCount: number;
|
||||
}>;
|
||||
/**
|
||||
* Get count of non-exception shifts in a series
|
||||
*/
|
||||
static getShiftCount(seriesId: string): Promise<number>;
|
||||
/**
|
||||
* Get series with all its shifts
|
||||
*/
|
||||
static getSeries(seriesId: string): Promise<{
|
||||
cut: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.CutCategory | null;
|
||||
description: string | null;
|
||||
createdByUserId: string | null;
|
||||
color: string;
|
||||
opacity: import("@prisma/client/runtime/library").Decimal;
|
||||
isPublic: boolean;
|
||||
isOfficial: boolean;
|
||||
geojson: string;
|
||||
bounds: string | null;
|
||||
showLocations: boolean;
|
||||
exportEnabled: boolean;
|
||||
assignedTo: string | null;
|
||||
filterSettings: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
lastCanvassed: Date | null;
|
||||
completionPercentage: number;
|
||||
} | null;
|
||||
shifts: ({
|
||||
signups: {
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
}[];
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
@@ -75,28 +58,51 @@ export declare class ShiftSeriesService {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
gancioEventId: number | null;
|
||||
meetingId: string | null;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
})[];
|
||||
cut: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.CutCategory | null;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
createdByUserId: string | null;
|
||||
color: string;
|
||||
opacity: import("@prisma/client/runtime/library").Decimal;
|
||||
isOfficial: boolean;
|
||||
geojson: string;
|
||||
bounds: string | null;
|
||||
showLocations: boolean;
|
||||
exportEnabled: boolean;
|
||||
assignedTo: string | null;
|
||||
filterSettings: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
lastCanvassed: Date | null;
|
||||
completionPercentage: number;
|
||||
} | null;
|
||||
} & {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
@@ -107,39 +113,18 @@ export declare class ShiftSeriesService {
|
||||
* Update series shifts based on edit mode
|
||||
*/
|
||||
static updateSeries(seriesId: string, input: UpdateShiftSeriesInput): Promise<{
|
||||
cut: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.CutCategory | null;
|
||||
description: string | null;
|
||||
createdByUserId: string | null;
|
||||
color: string;
|
||||
opacity: import("@prisma/client/runtime/library").Decimal;
|
||||
isPublic: boolean;
|
||||
isOfficial: boolean;
|
||||
geojson: string;
|
||||
bounds: string | null;
|
||||
showLocations: boolean;
|
||||
exportEnabled: boolean;
|
||||
assignedTo: string | null;
|
||||
filterSettings: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
lastCanvassed: Date | null;
|
||||
completionPercentage: number;
|
||||
} | null;
|
||||
shifts: ({
|
||||
signups: {
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
}[];
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
@@ -150,28 +135,51 @@ export declare class ShiftSeriesService {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
gancioEventId: number | null;
|
||||
meetingId: string | null;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
})[];
|
||||
cut: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
category: import(".prisma/client").$Enums.CutCategory | null;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
createdByUserId: string | null;
|
||||
color: string;
|
||||
opacity: import("@prisma/client/runtime/library").Decimal;
|
||||
isOfficial: boolean;
|
||||
geojson: string;
|
||||
bounds: string | null;
|
||||
showLocations: boolean;
|
||||
exportEnabled: boolean;
|
||||
assignedTo: string | null;
|
||||
filterSettings: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
lastCanvassed: Date | null;
|
||||
completionPercentage: number;
|
||||
} | null;
|
||||
} & {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"shift-series.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAK7F,qBAAa,kBAAkB;IAC7B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,EAAE;IA8CjE;;OAEG;WACU,YAAY,CAAC,KAAK,EAAE,sBAAsB,EAAE,SAAS,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;IA2D3E;;OAEG;WACU,SAAS,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAmBvC;;OAEG;WACU,YAAY,CACvB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0C/B;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,MAAM;CAW3C"}
|
||||
{"version":3,"file":"shift-series.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAK7F,qBAAa,kBAAkB;IAC7B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,EAAE;IA8CjE;;OAEG;WACU,YAAY,CAAC,KAAK,EAAE,sBAAsB,EAAE,SAAS,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;IA2D3E;;OAEG;WACU,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAM7D;;OAEG;WACU,SAAS,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAmBvC;;OAEG;WACU,YAAY,CACvB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0C/B;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,MAAM;CAW3C"}
|
||||
@@ -104,6 +104,14 @@ class ShiftSeriesService {
|
||||
generatedShiftsCount: result.shifts.length,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get count of non-exception shifts in a series
|
||||
*/
|
||||
static async getShiftCount(seriesId) {
|
||||
return database_1.prisma.shift.count({
|
||||
where: { seriesId, isException: false },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get series with all its shifts
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"file":"shifts.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shifts.routes.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,WAAW,4CAAW,CAAC;AA2J7B,QAAA,MAAM,eAAe,4CAAW,CAAC;AA2DjC,QAAA,MAAM,YAAY,4CAAW,CAAC;AA+B9B,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,YAAY,IAAI,kBAAkB,EAAE,eAAe,IAAI,qBAAqB,EAAE,CAAC"}
|
||||
{"version":3,"file":"shifts.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shifts.routes.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,WAAW,4CAAW,CAAC;AAuL7B,QAAA,MAAM,eAAe,4CAAW,CAAC;AA2DjC,QAAA,MAAM,YAAY,4CAAW,CAAC;AA0E9B,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,YAAY,IAAI,kBAAkB,EAAE,eAAe,IAAI,qBAAqB,EAAE,CAAC"}
|
||||
72
api/dist/modules/map/shifts/shifts.routes.js
vendored
72
api/dist/modules/map/shifts/shifts.routes.js
vendored
@@ -2,19 +2,20 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.shiftsVolunteerRouter = exports.shiftsPublicRouter = exports.shiftsAdminRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const client_1 = require("@prisma/client");
|
||||
const shifts_service_1 = require("./shifts.service");
|
||||
const shifts_schemas_1 = require("./shifts.schemas");
|
||||
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 database_1 = require("../../../config/database");
|
||||
const redis_1 = require("../../../config/redis");
|
||||
const roles_1 = require("../../../utils/roles");
|
||||
// --- Admin Router ---
|
||||
const adminRouter = (0, express_1.Router)();
|
||||
exports.shiftsAdminRouter = 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.SCHEDULING_ROLES));
|
||||
// GET /api/map/shifts — list paginated
|
||||
adminRouter.get('/', (0, validate_1.validate)(shifts_schemas_1.listShiftsSchema, 'query'), async (req, res, next) => {
|
||||
try {
|
||||
@@ -114,6 +115,28 @@ adminRouter.delete('/:id/signups/:signupId', async (req, res, next) => {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/map/shifts/:id/meeting — create and link a video briefing meeting
|
||||
adminRouter.post('/:id/meeting', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const meeting = await shifts_service_1.shiftsService.createMeetingForShift(id, req.user.id);
|
||||
res.status(201).json(meeting);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// DELETE /api/map/shifts/:id/meeting — remove video briefing from shift
|
||||
adminRouter.delete('/:id/meeting', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
await shifts_service_1.shiftsService.removeMeetingFromShift(id);
|
||||
res.status(204).send();
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/map/shifts/:id/email-details — email all volunteers
|
||||
adminRouter.post('/:id/email-details', async (req, res, next) => {
|
||||
try {
|
||||
@@ -195,4 +218,47 @@ publicRouter.post('/public/:id/signup', rate_limit_1.shiftSignupRateLimit, (0, v
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/map/shifts/public/related — related active campaigns
|
||||
publicRouter.get('/public/related', async (_req, res, next) => {
|
||||
try {
|
||||
const cacheKey = 'shifts:related:campaigns';
|
||||
try {
|
||||
const cached = await redis_1.redis.get(cacheKey);
|
||||
if (cached) {
|
||||
res.json(JSON.parse(cached));
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch { /* cache miss */ }
|
||||
const campaigns = await database_1.prisma.campaign.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_count: { select: { emails: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 3,
|
||||
});
|
||||
const result = {
|
||||
campaigns: campaigns.map(c => ({
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
description: c.description?.slice(0, 150) ?? null,
|
||||
emailCount: c._count.emails,
|
||||
})),
|
||||
};
|
||||
try {
|
||||
await redis_1.redis.setex(cacheKey, 300, JSON.stringify(result));
|
||||
}
|
||||
catch { /* non-critical */ }
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=shifts.routes.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ export declare const updateShiftSchema: z.ZodObject<{
|
||||
}>>;
|
||||
cutId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
status?: "CANCELLED" | "OPEN" | "FULL" | undefined;
|
||||
date?: string | undefined;
|
||||
location?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
@@ -57,7 +57,7 @@ export declare const updateShiftSchema: z.ZodObject<{
|
||||
endTime?: string | undefined;
|
||||
maxVolunteers?: number | undefined;
|
||||
}, {
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
status?: "CANCELLED" | "OPEN" | "FULL" | undefined;
|
||||
date?: string | undefined;
|
||||
location?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
@@ -85,11 +85,11 @@ export declare const listShiftsSchema: z.ZodObject<{
|
||||
page: number;
|
||||
sortBy: "date" | "createdAt" | "title";
|
||||
sortOrder: "asc" | "desc";
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
status?: "CANCELLED" | "OPEN" | "FULL" | undefined;
|
||||
search?: string | undefined;
|
||||
upcoming?: boolean | undefined;
|
||||
}, {
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
status?: "CANCELLED" | "OPEN" | "FULL" | undefined;
|
||||
search?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
page?: number | undefined;
|
||||
|
||||
87
api/dist/modules/map/shifts/shifts.service.d.ts
vendored
87
api/dist/modules/map/shifts/shifts.service.d.ts
vendored
@@ -9,6 +9,13 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
meeting: {
|
||||
id: string;
|
||||
title: string;
|
||||
isActive: boolean;
|
||||
slug: string;
|
||||
jitsiRoom: string;
|
||||
} | null;
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
date: Date;
|
||||
@@ -18,15 +25,17 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
gancioEventId: number | null;
|
||||
meetingId: string | null;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
})[];
|
||||
pagination: {
|
||||
page: number;
|
||||
@@ -43,6 +52,13 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
meeting: {
|
||||
id: string;
|
||||
title: string;
|
||||
isActive: boolean;
|
||||
slug: string;
|
||||
jitsiRoom: string;
|
||||
} | null;
|
||||
signups: ({
|
||||
user: {
|
||||
id: string;
|
||||
@@ -55,12 +71,12 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
})[];
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
@@ -71,15 +87,17 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
gancioEventId: number | null;
|
||||
meetingId: string | null;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
}>;
|
||||
create(data: CreateShiftInput, userId: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
@@ -90,15 +108,17 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
gancioEventId: number | null;
|
||||
meetingId: string | null;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
}>;
|
||||
update(id: string, data: UpdateShiftInput): Promise<{
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
@@ -109,17 +129,33 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdBy: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
gancioEventId: number | null;
|
||||
meetingId: string | null;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
}>;
|
||||
delete(id: string): Promise<void>;
|
||||
createMeetingForShift(shiftId: string, userId: string): Promise<{
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
title: string;
|
||||
isActive: boolean;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
createdByUserId: string;
|
||||
jitsiRoom: string;
|
||||
startTime: Date | null;
|
||||
endTime: Date | null;
|
||||
}>;
|
||||
removeMeetingFromShift(shiftId: string): Promise<void>;
|
||||
getStats(): Promise<{
|
||||
total: number;
|
||||
open: number;
|
||||
@@ -140,24 +176,24 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
})[]>;
|
||||
addSignup(shiftId: string, data: AddSignupInput): Promise<{
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
}>;
|
||||
removeSignup(signupId: string): Promise<void>;
|
||||
publicSignup(shiftId: string, data: PublicSignupInput): Promise<{
|
||||
@@ -166,12 +202,12 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
};
|
||||
isNewUser: boolean;
|
||||
}>;
|
||||
@@ -184,6 +220,11 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
meeting: {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
slug: string;
|
||||
} | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
@@ -194,12 +235,12 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
}>;
|
||||
cancelVolunteerSignup(shiftId: string, userId: string): Promise<void>;
|
||||
getMySignups(userId: string): Promise<({
|
||||
@@ -210,6 +251,11 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
meeting: {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
slug: string;
|
||||
} | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
@@ -220,12 +266,12 @@ export declare const shiftsService: {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
shiftId: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
})[]>;
|
||||
getPublicShifts(): Promise<{
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
@@ -234,6 +280,11 @@ export declare const shiftsService: {
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
meeting: {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
slug: string;
|
||||
} | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"shifts.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shifts.service.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,iBAAiB,EAClB,MAAM,kBAAkB,CAAC;AAY1B,eAAO,MAAM,aAAa;qBACD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuDnB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyBN,gBAAgB,UAAU,MAAM;;;;;;;;;;;;;;;;;;;eAmBlC,MAAM,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;eA6B9B,MAAM;;;;;;;;;wBAsBG,MAAM;;;;;;;;;;;;;;;;;;;uBAaP,MAAM,QAAQ,cAAc;;;;;;;;;;;;2BA6DxB,MAAM;0BAyBP,MAAM,QAAQ,iBAAiB;;;;;;;;;;;;;;;gCAqJzB,MAAM,aAAa,MAAM;oCA4BrB,MAAM;;;;;;;;;;;;;6BA2Cb,MAAM,UAAU,MAAM;;;;;;;;;;;;mCAoGhB,MAAM,UAAU,MAAM;yBA0BhC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA4DA,MAAM;;;;+BAsEN,MAAM,WAAW,MAAM;;mBAiBf,MAAM;oBAAU,GAAG,EAAE;;;CAe/D,CAAC"}
|
||||
{"version":3,"file":"shifts.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shifts.service.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,iBAAiB,EAClB,MAAM,kBAAkB,CAAC;AAoB1B,eAAO,MAAM,aAAa;qBACD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwDnB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BN,gBAAgB,UAAU,MAAM;;;;;;;;;;;;;;;;;;;;;eA2ClC,MAAM,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;;;eA8C9B,MAAM;mCAwBc,MAAM,UAAU,MAAM;;;;;;;;;;;;;oCAyBrB,MAAM;;;;;;;;;wBA4BlB,MAAM;;;;;;;;;;;;;;;;;;;uBAaP,MAAM,QAAQ,cAAc;;;;;;;;;;;;2BA2ExB,MAAM;0BA4BP,MAAM,QAAQ,iBAAiB;;;;;;;;;;;;;;;gCAuQzB,MAAM,aAAa,MAAM;oCAuGrB,MAAM;;;;;;;;;;;;;;;;;;6BA4Cb,MAAM,UAAU,MAAM;;;;;;;;;;;;mCAsLhB,MAAM,UAAU,MAAM;yBAqGhC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA8DA,MAAM;;;;+BAuDN,MAAM,WAAW,MAAM;;mBAiBf,MAAM;oBAAU,GAAG,EAAE;;;CAe/D,CAAC"}
|
||||
589
api/dist/modules/map/shifts/shifts.service.js
vendored
589
api/dist/modules/map/shifts/shifts.service.js
vendored
@@ -9,10 +9,21 @@ const client_1 = require("@prisma/client");
|
||||
const database_1 = require("../../../config/database");
|
||||
const error_handler_1 = require("../../../middleware/error-handler");
|
||||
const email_service_1 = require("../../../services/email.service");
|
||||
const settings_service_1 = require("../../settings/settings.service");
|
||||
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 logger_1 = require("../../../utils/logger");
|
||||
const metrics_1 = require("../../../utils/metrics");
|
||||
const rocketchat_webhook_service_1 = require("../../../services/rocketchat-webhook.service");
|
||||
const listmonk_event_sync_service_1 = require("../../../services/listmonk-event-sync.service");
|
||||
const gancio_client_1 = require("../../../services/gancio.client");
|
||||
const unified_calendar_service_1 = require("../../events/unified-calendar.service");
|
||||
const group_service_1 = require("../../social/group.service");
|
||||
const achievements_service_1 = require("../../social/achievements.service");
|
||||
const slug_1 = require("../../../utils/slug");
|
||||
const settings_service_1 = require("../../settings/settings.service");
|
||||
const sms_notification_service_1 = require("../../../services/sms-notification.service");
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const adjectives = ['Blue', 'Red', 'Green', 'Swift', 'Bright', 'Bold', 'Calm', 'Fair'];
|
||||
const nouns = ['Eagle', 'River', 'Mountain', 'Star', 'Forest', 'Lake', 'Wolf', 'Hawk'];
|
||||
function generateReadablePassword() {
|
||||
@@ -21,6 +32,13 @@ function generateReadablePassword() {
|
||||
const num = Math.floor(Math.random() * 90) + 10;
|
||||
return `${adj}${noun}${num}`;
|
||||
}
|
||||
const meetingSelect = {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
isActive: true,
|
||||
jitsiRoom: true,
|
||||
};
|
||||
exports.shiftsService = {
|
||||
async findAll(filters) {
|
||||
const { page, limit, search, status, upcoming, sortBy, sortOrder } = filters;
|
||||
@@ -50,6 +68,7 @@ exports.shiftsService = {
|
||||
orderBy,
|
||||
include: {
|
||||
cut: { select: { id: true, name: true } },
|
||||
meeting: { select: meetingSelect },
|
||||
_count: {
|
||||
select: {
|
||||
signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
|
||||
@@ -74,6 +93,7 @@ exports.shiftsService = {
|
||||
where: { id },
|
||||
include: {
|
||||
cut: { select: { id: true, name: true } },
|
||||
meeting: { select: meetingSelect },
|
||||
signups: {
|
||||
where: { status: client_1.SignupStatus.CONFIRMED },
|
||||
include: { user: { select: { id: true, email: true, name: true, phone: true } } },
|
||||
@@ -106,6 +126,28 @@ exports.shiftsService = {
|
||||
createdBy: userId,
|
||||
},
|
||||
});
|
||||
// Gancio event sync (fire-and-forget)
|
||||
if (gancio_client_1.gancioClient.enabled) {
|
||||
gancio_client_1.gancioClient.createEvent({
|
||||
title: shift.title,
|
||||
description: shift.description,
|
||||
location: shift.location,
|
||||
date: shift.date,
|
||||
startTime: shift.startTime,
|
||||
endTime: shift.endTime,
|
||||
}).then(async (eventId) => {
|
||||
if (eventId) {
|
||||
await database_1.prisma.shift.update({
|
||||
where: { id: shift.id },
|
||||
data: { gancioEventId: eventId },
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
logger_1.logger.warn('Gancio sync on shift create failed:', err);
|
||||
});
|
||||
}
|
||||
// Bust unified calendar cache
|
||||
unified_calendar_service_1.unifiedCalendarService.bustCache().catch(() => { });
|
||||
return shift;
|
||||
},
|
||||
async update(id, data) {
|
||||
@@ -130,6 +172,21 @@ exports.shiftsService = {
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
// Gancio event sync (fire-and-forget)
|
||||
if (gancio_client_1.gancioClient.enabled && shift.gancioEventId) {
|
||||
gancio_client_1.gancioClient.updateEvent(shift.gancioEventId, {
|
||||
title: shift.title,
|
||||
description: shift.description,
|
||||
location: shift.location,
|
||||
date: shift.date,
|
||||
startTime: shift.startTime,
|
||||
endTime: shift.endTime,
|
||||
}).catch((err) => {
|
||||
logger_1.logger.warn('Gancio sync on shift update failed:', err);
|
||||
});
|
||||
}
|
||||
// Bust unified calendar cache
|
||||
unified_calendar_service_1.unifiedCalendarService.bustCache().catch(() => { });
|
||||
return shift;
|
||||
},
|
||||
async delete(id) {
|
||||
@@ -137,7 +194,56 @@ exports.shiftsService = {
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
}
|
||||
// Delete Gancio event before deleting shift (fire-and-forget)
|
||||
if (gancio_client_1.gancioClient.enabled && existing.gancioEventId) {
|
||||
gancio_client_1.gancioClient.deleteEvent(existing.gancioEventId).catch((err) => {
|
||||
logger_1.logger.warn('Gancio sync on shift delete failed:', err);
|
||||
});
|
||||
}
|
||||
// Delete associated meeting if exists
|
||||
if (existing.meetingId) {
|
||||
await database_1.prisma.meeting.delete({ where: { id: existing.meetingId } }).catch(() => { });
|
||||
}
|
||||
await database_1.prisma.shift.delete({ where: { id } });
|
||||
// Bust unified calendar cache
|
||||
unified_calendar_service_1.unifiedCalendarService.bustCache().catch(() => { });
|
||||
},
|
||||
async createMeetingForShift(shiftId, userId) {
|
||||
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
||||
if (!shift)
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
if (shift.meetingId)
|
||||
throw new error_handler_1.AppError(400, 'Shift already has a meeting', 'MEETING_EXISTS');
|
||||
const settings = await settings_service_1.siteSettingsService.get();
|
||||
if (!settings.enableMeet)
|
||||
throw new error_handler_1.AppError(400, 'Video meetings are not enabled', 'MEET_DISABLED');
|
||||
const meeting = await database_1.prisma.meeting.create({
|
||||
data: {
|
||||
slug: (0, slug_1.generateSlug)(shift.title),
|
||||
title: `${shift.title} — Video Briefing`,
|
||||
jitsiRoom: crypto_1.default.randomUUID(),
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
await database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: { meetingId: meeting.id },
|
||||
});
|
||||
return meeting;
|
||||
},
|
||||
async removeMeetingFromShift(shiftId) {
|
||||
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
||||
if (!shift)
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
if (!shift.meetingId)
|
||||
throw new error_handler_1.AppError(400, 'Shift has no meeting', 'NO_MEETING');
|
||||
const meetingId = shift.meetingId;
|
||||
await database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: { meetingId: null },
|
||||
});
|
||||
// Delete the meeting record
|
||||
await database_1.prisma.meeting.delete({ where: { id: meetingId } }).catch(() => { });
|
||||
},
|
||||
async getStats() {
|
||||
const [total, open, full, cancelled, upcoming, totalSignups] = await Promise.all([
|
||||
@@ -214,6 +320,18 @@ exports.shiftsService = {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
// Listmonk event sync
|
||||
listmonk_event_sync_service_1.listmonkEventSyncService.onShiftSignup({
|
||||
email: data.userEmail,
|
||||
name: data.userName || data.userEmail,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: new Date(shift.date).toISOString().split('T')[0],
|
||||
}).catch(() => { });
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
|
||||
// Achievement check (fire-and-forget)
|
||||
if (user?.id)
|
||||
achievements_service_1.achievementsService.checkAndUnlock(user.id, ['shifts']).catch(() => { });
|
||||
return signup;
|
||||
},
|
||||
async removeSignup(signupId) {
|
||||
@@ -237,6 +355,8 @@ exports.shiftsService = {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncShiftTeam(signup.shiftId).catch(() => { });
|
||||
},
|
||||
async publicSignup(shiftId, data) {
|
||||
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
||||
@@ -279,6 +399,7 @@ exports.shiftsService = {
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
role: 'TEMP',
|
||||
roles: JSON.parse(JSON.stringify(['TEMP'])),
|
||||
createdVia: 'PUBLIC_SHIFT_SIGNUP',
|
||||
expiresAt: shiftDate,
|
||||
},
|
||||
@@ -339,38 +460,142 @@ exports.shiftsService = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const htmlTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'html');
|
||||
const txtTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'txt');
|
||||
let orgName = 'Changemaker Lite';
|
||||
try {
|
||||
orgName = (await settings_service_1.siteSettingsService.get()).organizationName || orgName;
|
||||
}
|
||||
catch { /* use default */ }
|
||||
const vars = {
|
||||
USER_NAME: data.name,
|
||||
USER_EMAIL: data.email,
|
||||
SHIFT_TITLE: shift.title,
|
||||
SHIFT_DATE: dateStr,
|
||||
SHIFT_TIME: `${shift.startTime} — ${shift.endTime}`,
|
||||
SHIFT_LOCATION: shift.location || 'TBD',
|
||||
IS_NEW_USER: isNewUser ? 'true' : '',
|
||||
TEMP_PASSWORD: tempPassword || '',
|
||||
LOGIN_URL: `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
|
||||
ORGANIZATION_NAME: orgName,
|
||||
};
|
||||
const html = email_service_1.emailService.processTemplate(htmlTemplate, vars);
|
||||
const text = email_service_1.emailService.processTemplate(txtTemplate, vars);
|
||||
await email_service_1.emailService.sendEmail({
|
||||
to: data.email,
|
||||
subject: `Signup Confirmed — ${shift.title}`,
|
||||
html,
|
||||
text,
|
||||
await email_service_1.emailService.sendShiftSignupConfirmation({
|
||||
recipientEmail: data.email,
|
||||
recipientName: data.name,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
isNewUser,
|
||||
tempPassword,
|
||||
loginUrl: `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to send shift signup confirmation email:', err);
|
||||
}
|
||||
// SMS signup confirmation (fire-and-forget)
|
||||
if (data.phone) {
|
||||
const shiftDate = new Date(shift.date);
|
||||
const smsDateStr = shiftDate.toLocaleDateString('en-CA', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
sms_notification_service_1.smsNotificationService.sendShiftSignupConfirmation(data.phone, {
|
||||
name: data.name,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: smsDateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
}).catch(err => logger_1.logger.error('SMS signup confirmation failed:', err));
|
||||
}
|
||||
// Notify Rocket.Chat
|
||||
const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
|
||||
rocketchat_webhook_service_1.rocketchatWebhookService.onShiftSignup({
|
||||
userName: data.name || data.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: shiftDateStr,
|
||||
}).catch(() => { });
|
||||
// Notification: admin shift signup alert
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftSignup')) {
|
||||
const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
|
||||
if (adminEmails.length > 0) {
|
||||
const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'admin-shift-signup',
|
||||
adminEmails,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
volunteerName: data.name,
|
||||
volunteerEmail: data.email,
|
||||
signupSource: 'Public Form',
|
||||
adminUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue admin shift signup notification:', err);
|
||||
}
|
||||
// Notification: schedule 24h pre-shift reminder
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerShiftReminder')) {
|
||||
const shiftDatetime = new Date(shift.date);
|
||||
const [startH, startM] = shift.startTime.split(':').map(Number);
|
||||
shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);
|
||||
await notification_queue_service_1.notificationQueueService.scheduleShiftReminder({
|
||||
type: 'volunteer-shift-reminder',
|
||||
recipientEmail: data.email,
|
||||
recipientName: data.name,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: shiftDatetime.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
|
||||
shiftStartTime: shift.startTime,
|
||||
shiftEndTime: shift.endTime,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
shiftDescription: shift.description || '',
|
||||
currentVolunteers: shift.currentVolunteers + 1,
|
||||
maxVolunteers: shift.maxVolunteers,
|
||||
shiftStatus: shift.status,
|
||||
}, shiftDatetime);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to schedule shift reminder:', err);
|
||||
}
|
||||
// SMS shift reminder (fire-and-forget, delay calculated by notification service)
|
||||
if (data.phone) {
|
||||
const smsShiftDatetime = new Date(shift.date);
|
||||
const [smsH, smsM] = shift.startTime.split(':').map(Number);
|
||||
smsShiftDatetime.setHours(smsH || 0, smsM || 0, 0, 0);
|
||||
sms_notification_service_1.smsNotificationService.sendShiftReminder(data.phone, {
|
||||
name: data.name,
|
||||
shiftTitle: shift.title,
|
||||
shiftTime: shift.startTime,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
}, smsShiftDatetime).catch(err => logger_1.logger.error('SMS shift reminder failed:', err));
|
||||
}
|
||||
// Notification: schedule post-shift thank-you (2h after end)
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerShiftThankYou')) {
|
||||
const shiftEndDatetime = new Date(shift.date);
|
||||
const [endH, endM] = shift.endTime.split(':').map(Number);
|
||||
shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const signupUrl = `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
|
||||
await notification_queue_service_1.notificationQueueService.scheduleShiftThankYou({
|
||||
type: 'volunteer-shift-thank-you',
|
||||
volunteerEmail: data.email,
|
||||
volunteerName: data.name,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
signupUrl,
|
||||
}, shiftEndDatetime);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to schedule shift thank-you:', err);
|
||||
}
|
||||
(0, metrics_1.recordShiftSignup)();
|
||||
// Listmonk event sync
|
||||
listmonk_event_sync_service_1.listmonkEventSyncService.onShiftSignup({
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: new Date(shift.date).toISOString().split('T')[0],
|
||||
}).catch(() => { });
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
|
||||
// Achievement check (fire-and-forget)
|
||||
if (user?.id)
|
||||
achievements_service_1.achievementsService.checkAndUnlock(user.id, ['shifts']).catch(() => { });
|
||||
return { signup, isNewUser };
|
||||
},
|
||||
async cancelPublicSignup(shiftId, userEmail) {
|
||||
@@ -383,6 +608,7 @@ exports.shiftsService = {
|
||||
if (signup.status === client_1.SignupStatus.CANCELLED) {
|
||||
throw new error_handler_1.AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
|
||||
}
|
||||
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
||||
await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: signup.id },
|
||||
@@ -396,6 +622,75 @@ exports.shiftsService = {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
// Notification: cancellation acknowledgement + cancel reminder
|
||||
try {
|
||||
if (shift && await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerCancellation')) {
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const signupUrl = `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'volunteer-cancellation',
|
||||
volunteerEmail: userEmail,
|
||||
volunteerName: signup.userName || userEmail,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
signupUrl,
|
||||
});
|
||||
}
|
||||
// Cancel the pending shift reminder
|
||||
if (shift) {
|
||||
const shiftDatetime = new Date(shift.date);
|
||||
const [startH, startM] = shift.startTime.split(':').map(Number);
|
||||
shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);
|
||||
await notification_queue_service_1.notificationQueueService.cancelShiftReminder(userEmail, shiftDatetime);
|
||||
}
|
||||
// Cancel the pending shift thank-you
|
||||
if (shift) {
|
||||
const shiftEndDatetime = new Date(shift.date);
|
||||
const [endH, endM] = shift.endTime.split(':').map(Number);
|
||||
shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);
|
||||
await notification_queue_service_1.notificationQueueService.cancelShiftThankYou(userEmail, shiftEndDatetime);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue cancellation notification:', err);
|
||||
}
|
||||
// Notify Rocket.Chat of cancellation
|
||||
if (shift) {
|
||||
const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
|
||||
rocketchat_webhook_service_1.rocketchatWebhookService.onShiftCancellation({
|
||||
userName: signup.userName || userEmail,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: shiftDateStr,
|
||||
}).catch(() => { });
|
||||
}
|
||||
// Notification: admin shift cancellation alert
|
||||
try {
|
||||
if (shift && await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftCancellation')) {
|
||||
const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
|
||||
if (adminEmails.length > 0) {
|
||||
const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'admin-shift-cancellation',
|
||||
adminEmails,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
volunteerName: signup.userName || userEmail,
|
||||
volunteerEmail: userEmail,
|
||||
cancellationSource: 'Public Form',
|
||||
adminUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue admin shift cancellation notification:', err);
|
||||
}
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
|
||||
},
|
||||
async getUpcomingForVolunteer(userId) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
||||
@@ -418,6 +713,7 @@ exports.shiftsService = {
|
||||
maxVolunteers: true,
|
||||
currentVolunteers: true,
|
||||
status: true,
|
||||
meeting: { select: { id: true, slug: true, isActive: true } },
|
||||
},
|
||||
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
@@ -504,41 +800,115 @@ exports.shiftsService = {
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||||
});
|
||||
const htmlTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'html');
|
||||
const txtTemplate = email_service_1.emailService.loadTemplate('shift-signup-confirmation', 'txt');
|
||||
let orgName = 'Changemaker Lite';
|
||||
try {
|
||||
orgName = (await settings_service_1.siteSettingsService.get()).organizationName || orgName;
|
||||
}
|
||||
catch { /* default */ }
|
||||
const vars = {
|
||||
USER_NAME: user.name || user.email,
|
||||
USER_EMAIL: user.email,
|
||||
SHIFT_TITLE: shift.title,
|
||||
SHIFT_DATE: dateStr,
|
||||
SHIFT_TIME: `${shift.startTime} — ${shift.endTime}`,
|
||||
SHIFT_LOCATION: shift.location || 'TBD',
|
||||
IS_NEW_USER: '',
|
||||
TEMP_PASSWORD: '',
|
||||
LOGIN_URL: `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
|
||||
ORGANIZATION_NAME: orgName,
|
||||
};
|
||||
const html = email_service_1.emailService.processTemplate(htmlTemplate, vars);
|
||||
const text = email_service_1.emailService.processTemplate(txtTemplate, vars);
|
||||
await email_service_1.emailService.sendEmail({
|
||||
to: user.email,
|
||||
subject: `Signup Confirmed — ${shift.title}`,
|
||||
html,
|
||||
text,
|
||||
await email_service_1.emailService.sendShiftSignupConfirmation({
|
||||
recipientEmail: user.email,
|
||||
recipientName: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
isNewUser: false,
|
||||
loginUrl: `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to send volunteer shift signup confirmation email:', err);
|
||||
}
|
||||
// Notify Rocket.Chat
|
||||
const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
|
||||
rocketchat_webhook_service_1.rocketchatWebhookService.onShiftSignup({
|
||||
userName: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: shiftDateStr,
|
||||
}).catch(() => { });
|
||||
// Notification: admin shift signup alert
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftSignup')) {
|
||||
const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
|
||||
if (adminEmails.length > 0) {
|
||||
const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'admin-shift-signup',
|
||||
adminEmails,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
volunteerName: user.name || user.email,
|
||||
volunteerEmail: user.email,
|
||||
signupSource: 'Authenticated Volunteer',
|
||||
adminUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue admin shift signup notification:', err);
|
||||
}
|
||||
// Notification: schedule 24h pre-shift reminder
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerShiftReminder')) {
|
||||
const shiftDatetime = new Date(shift.date);
|
||||
const [startH, startM] = shift.startTime.split(':').map(Number);
|
||||
shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);
|
||||
await notification_queue_service_1.notificationQueueService.scheduleShiftReminder({
|
||||
type: 'volunteer-shift-reminder',
|
||||
recipientEmail: user.email,
|
||||
recipientName: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: shiftDatetime.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
|
||||
shiftStartTime: shift.startTime,
|
||||
shiftEndTime: shift.endTime,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
shiftDescription: shift.description || '',
|
||||
currentVolunteers: shift.currentVolunteers + 1,
|
||||
maxVolunteers: shift.maxVolunteers,
|
||||
shiftStatus: shift.status,
|
||||
}, shiftDatetime);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to schedule shift reminder:', err);
|
||||
}
|
||||
// Notification: schedule post-shift thank-you (2h after end)
|
||||
try {
|
||||
if (await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerShiftThankYou')) {
|
||||
const shiftEndDatetime = new Date(shift.date);
|
||||
const [endH, endM] = shift.endTime.split(':').map(Number);
|
||||
shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const signupUrl = `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
|
||||
await notification_queue_service_1.notificationQueueService.scheduleShiftThankYou({
|
||||
type: 'volunteer-shift-thank-you',
|
||||
volunteerEmail: user.email,
|
||||
volunteerName: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
signupUrl,
|
||||
}, shiftEndDatetime);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to schedule shift thank-you:', err);
|
||||
}
|
||||
// Listmonk event sync
|
||||
listmonk_event_sync_service_1.listmonkEventSyncService.onShiftSignup({
|
||||
email: user.email,
|
||||
name: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: new Date(shift.date).toISOString().split('T')[0],
|
||||
}).catch(() => { });
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
|
||||
// Achievement check (fire-and-forget)
|
||||
achievements_service_1.achievementsService.checkAndUnlock(userId, ['shifts']).catch(() => { });
|
||||
return signup;
|
||||
},
|
||||
async cancelVolunteerSignup(shiftId, userId) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } });
|
||||
if (!user)
|
||||
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
||||
const signup = await database_1.prisma.shiftSignup.findUnique({
|
||||
@@ -548,6 +918,7 @@ exports.shiftsService = {
|
||||
throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
|
||||
if (signup.status === client_1.SignupStatus.CANCELLED)
|
||||
throw new error_handler_1.AppError(400, 'Already cancelled', 'ALREADY_CANCELLED');
|
||||
const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
|
||||
await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: signup.id },
|
||||
@@ -561,6 +932,75 @@ exports.shiftsService = {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
// Notification: cancellation acknowledgement + cancel reminder
|
||||
try {
|
||||
if (shift && await (0, notification_helper_1.isNotificationEnabled)('notifyVolunteerCancellation')) {
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const signupUrl = `${env_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'volunteer-cancellation',
|
||||
volunteerEmail: user.email,
|
||||
volunteerName: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftTime: `${shift.startTime} — ${shift.endTime}`,
|
||||
signupUrl,
|
||||
});
|
||||
}
|
||||
// Cancel the pending shift reminder
|
||||
if (shift) {
|
||||
const shiftDatetime = new Date(shift.date);
|
||||
const [startH, startM] = shift.startTime.split(':').map(Number);
|
||||
shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);
|
||||
await notification_queue_service_1.notificationQueueService.cancelShiftReminder(user.email, shiftDatetime);
|
||||
}
|
||||
// Cancel the pending shift thank-you
|
||||
if (shift) {
|
||||
const shiftEndDatetime = new Date(shift.date);
|
||||
const [endH, endM] = shift.endTime.split(':').map(Number);
|
||||
shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);
|
||||
await notification_queue_service_1.notificationQueueService.cancelShiftThankYou(user.email, shiftEndDatetime);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue cancellation notification:', err);
|
||||
}
|
||||
// Notify Rocket.Chat of cancellation
|
||||
if (shift) {
|
||||
const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
|
||||
rocketchat_webhook_service_1.rocketchatWebhookService.onShiftCancellation({
|
||||
userName: user.name || user.email,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: shiftDateStr,
|
||||
}).catch(() => { });
|
||||
}
|
||||
// Notification: admin shift cancellation alert
|
||||
try {
|
||||
if (shift && await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftCancellation')) {
|
||||
const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
|
||||
if (adminEmails.length > 0) {
|
||||
const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||
type: 'admin-shift-cancellation',
|
||||
adminEmails,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
volunteerName: user.name || user.email,
|
||||
volunteerEmail: user.email,
|
||||
cancellationSource: 'Volunteer Portal',
|
||||
adminUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to enqueue admin shift cancellation notification:', err);
|
||||
}
|
||||
// Social group sync (fire-and-forget)
|
||||
group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
|
||||
},
|
||||
async getMySignups(userId) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
||||
@@ -588,6 +1028,7 @@ exports.shiftsService = {
|
||||
maxVolunteers: true,
|
||||
currentVolunteers: true,
|
||||
status: true,
|
||||
meeting: { select: { id: true, slug: true, isActive: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -613,6 +1054,7 @@ exports.shiftsService = {
|
||||
maxVolunteers: true,
|
||||
currentVolunteers: true,
|
||||
status: true,
|
||||
meeting: { select: { id: true, slug: true, isActive: true } },
|
||||
},
|
||||
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
@@ -637,37 +1079,22 @@ exports.shiftsService = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const htmlTemplate = email_service_1.emailService.loadTemplate('shift-details', 'html');
|
||||
const txtTemplate = email_service_1.emailService.loadTemplate('shift-details', 'txt');
|
||||
let orgName = 'Changemaker Lite';
|
||||
try {
|
||||
orgName = (await settings_service_1.siteSettingsService.get()).organizationName || orgName;
|
||||
}
|
||||
catch { /* use default */ }
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
for (const signup of shift.signups) {
|
||||
try {
|
||||
const vars = {
|
||||
USER_NAME: signup.userName || signup.userEmail,
|
||||
SHIFT_TITLE: shift.title,
|
||||
SHIFT_DATE: dateStr,
|
||||
SHIFT_START_TIME: shift.startTime,
|
||||
SHIFT_END_TIME: shift.endTime,
|
||||
SHIFT_LOCATION: shift.location || 'TBD',
|
||||
SHIFT_DESCRIPTION: shift.description || '',
|
||||
CURRENT_VOLUNTEERS: shift.currentVolunteers.toString(),
|
||||
MAX_VOLUNTEERS: shift.maxVolunteers.toString(),
|
||||
SHIFT_STATUS: shift.status,
|
||||
ORGANIZATION_NAME: orgName,
|
||||
};
|
||||
const html = email_service_1.emailService.processTemplate(htmlTemplate, vars);
|
||||
const text = email_service_1.emailService.processTemplate(txtTemplate, vars);
|
||||
const result = await email_service_1.emailService.sendEmail({
|
||||
to: signup.userEmail,
|
||||
subject: `Shift Details — ${shift.title}`,
|
||||
html,
|
||||
text,
|
||||
const result = await email_service_1.emailService.sendShiftDetailsEmail({
|
||||
recipientEmail: signup.userEmail,
|
||||
recipientName: signup.userName || signup.userEmail,
|
||||
shiftTitle: shift.title,
|
||||
shiftDate: dateStr,
|
||||
shiftStartTime: shift.startTime,
|
||||
shiftEndTime: shift.endTime,
|
||||
shiftLocation: shift.location || 'TBD',
|
||||
shiftDescription: shift.description || '',
|
||||
currentVolunteers: shift.currentVolunteers,
|
||||
maxVolunteers: shift.maxVolunteers,
|
||||
shiftStatus: shift.status,
|
||||
});
|
||||
if (result.success) {
|
||||
sent++;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user