Tonne of debugging - getting ready for the production builds
This commit is contained in:
3
api/dist/modules/map/shifts/shift-series.routes.d.ts
vendored
Normal file
3
api/dist/modules/map/shifts/shift-series.routes.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=shift-series.routes.d.ts.map
|
||||
1
api/dist/modules/map/shifts/shift-series.routes.d.ts.map
vendored
Normal file
1
api/dist/modules/map/shifts/shift-series.routes.d.ts.map
vendored
Normal file
@@ -0,0 +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"}
|
||||
54
api/dist/modules/map/shifts/shift-series.routes.js
vendored
Normal file
54
api/dist/modules/map/shifts/shift-series.routes.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
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 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));
|
||||
// Create series
|
||||
router.post('/', (0, validate_1.validate)(shift_series_schemas_1.createShiftSeriesSchema), async (req, res, next) => {
|
||||
try {
|
||||
const result = await shift_series_service_1.ShiftSeriesService.createSeries(req.body, req.user?.id);
|
||||
res.json(result);
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// Get series
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const series = await shift_series_service_1.ShiftSeriesService.getSeries(req.params.id);
|
||||
res.json({ series });
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// Update series
|
||||
router.put('/:id', (0, validate_1.validate)(shift_series_schemas_1.updateShiftSeriesSchema), async (req, res, next) => {
|
||||
try {
|
||||
const series = await shift_series_service_1.ShiftSeriesService.updateSeries(req.params.id, req.body);
|
||||
res.json({ series });
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// Delete series
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
await shift_series_service_1.ShiftSeriesService.deleteSeries(req.params.id);
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=shift-series.routes.js.map
|
||||
1
api/dist/modules/map/shifts/shift-series.routes.js.map
vendored
Normal file
1
api/dist/modules/map/shifts/shift-series.routes.js.map
vendored
Normal file
@@ -0,0 +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"}
|
||||
108
api/dist/modules/map/shifts/shift-series.schemas.d.ts
vendored
Normal file
108
api/dist/modules/map/shifts/shift-series.schemas.d.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
import { z } from 'zod';
|
||||
export declare const createShiftSeriesSchema: z.ZodEffects<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
startTime: z.ZodString;
|
||||
endTime: z.ZodString;
|
||||
location: z.ZodOptional<z.ZodString>;
|
||||
maxVolunteers: z.ZodNumber;
|
||||
isPublic: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
cutId: z.ZodOptional<z.ZodString>;
|
||||
frequency: z.ZodNativeEnum<{
|
||||
DAILY: "DAILY";
|
||||
WEEKLY: "WEEKLY";
|
||||
MONTHLY: "MONTHLY";
|
||||
}>;
|
||||
daysOfWeek: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
|
||||
startDate: z.ZodString;
|
||||
endDate: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title: string;
|
||||
startDate: string;
|
||||
isPublic: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
startDate: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
}>, {
|
||||
title: string;
|
||||
startDate: string;
|
||||
isPublic: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
startDate: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | undefined;
|
||||
daysOfWeek?: number[] | undefined;
|
||||
}>;
|
||||
export declare const updateShiftSeriesSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
startTime: z.ZodOptional<z.ZodString>;
|
||||
endTime: z.ZodOptional<z.ZodString>;
|
||||
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
maxVolunteers: z.ZodOptional<z.ZodNumber>;
|
||||
isPublic: z.ZodOptional<z.ZodBoolean>;
|
||||
cutId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
editMode: z.ZodEnum<["THIS", "FUTURE", "ALL"]>;
|
||||
fromDate: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
editMode: "THIS" | "FUTURE" | "ALL";
|
||||
location?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | null | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | null | undefined;
|
||||
startTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
maxVolunteers?: number | undefined;
|
||||
fromDate?: string | undefined;
|
||||
}, {
|
||||
editMode: "THIS" | "FUTURE" | "ALL";
|
||||
location?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | null | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | null | undefined;
|
||||
startTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
maxVolunteers?: number | undefined;
|
||||
fromDate?: string | undefined;
|
||||
}>;
|
||||
export type CreateShiftSeriesInput = z.infer<typeof createShiftSeriesSchema>;
|
||||
export type UpdateShiftSeriesInput = z.infer<typeof updateShiftSeriesSchema>;
|
||||
//# sourceMappingURL=shift-series.schemas.d.ts.map
|
||||
1
api/dist/modules/map/shifts/shift-series.schemas.d.ts.map
vendored
Normal file
1
api/dist/modules/map/shifts/shift-series.schemas.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"shift-series.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBnC,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWlC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC7E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
||||
39
api/dist/modules/map/shifts/shift-series.schemas.js
vendored
Normal file
39
api/dist/modules/map/shifts/shift-series.schemas.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateShiftSeriesSchema = exports.createShiftSeriesSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const client_1 = require("@prisma/client");
|
||||
exports.createShiftSeriesSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1, 'Title is required'),
|
||||
description: zod_1.z.string().optional(),
|
||||
startTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM'),
|
||||
endTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM'),
|
||||
location: zod_1.z.string().optional(),
|
||||
maxVolunteers: zod_1.z.number().int().min(1, 'Must have at least 1 volunteer spot'),
|
||||
isPublic: zod_1.z.boolean().optional().default(false),
|
||||
cutId: zod_1.z.string().optional(),
|
||||
// Recurrence
|
||||
frequency: zod_1.z.nativeEnum(client_1.RecurrenceFrequency),
|
||||
daysOfWeek: zod_1.z.array(zod_1.z.number().int().min(0).max(6)).optional(), // 0=Sun, 6=Sat
|
||||
startDate: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Start date must be YYYY-MM-DD'),
|
||||
endDate: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'End date must be YYYY-MM-DD').optional(),
|
||||
}).refine((data) => {
|
||||
// Weekly requires daysOfWeek
|
||||
if (data.frequency === client_1.RecurrenceFrequency.WEEKLY && !data.daysOfWeek?.length) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, { message: 'Weekly recurrence requires at least one day of week', path: ['daysOfWeek'] });
|
||||
exports.updateShiftSeriesSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).optional(),
|
||||
description: zod_1.z.string().nullable().optional(),
|
||||
startTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/).optional(),
|
||||
endTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/).optional(),
|
||||
location: zod_1.z.string().nullable().optional(),
|
||||
maxVolunteers: zod_1.z.number().int().min(1).optional(),
|
||||
isPublic: zod_1.z.boolean().optional(),
|
||||
cutId: zod_1.z.string().nullable().optional(),
|
||||
editMode: zod_1.z.enum(['THIS', 'FUTURE', 'ALL']),
|
||||
fromDate: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), // Required for FUTURE
|
||||
});
|
||||
//# sourceMappingURL=shift-series.schemas.js.map
|
||||
1
api/dist/modules/map/shifts/shift-series.schemas.js.map
vendored
Normal file
1
api/dist/modules/map/shifts/shift-series.schemas.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"shift-series.schemas.js","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shift-series.schemas.ts"],"names":[],"mappings":"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"}
|
||||
186
api/dist/modules/map/shifts/shift-series.service.d.ts
vendored
Normal file
186
api/dist/modules/map/shifts/shift-series.service.d.ts
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { CreateShiftSeriesInput, UpdateShiftSeriesInput } from './shift-series.schemas';
|
||||
export declare class ShiftSeriesService {
|
||||
/**
|
||||
* Generate occurrence dates based on recurrence rules
|
||||
*/
|
||||
static generateOccurrences(input: CreateShiftSeriesInput): Date[];
|
||||
/**
|
||||
* Create a shift series and generate all individual shifts
|
||||
*/
|
||||
static createSeries(input: CreateShiftSeriesInput, createdBy?: string): Promise<{
|
||||
series: {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: import(".prisma/client").$Enums.RecurrenceFrequency;
|
||||
daysOfWeek: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
};
|
||||
generatedShiftsCount: 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;
|
||||
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;
|
||||
date: Date;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
})[];
|
||||
} & {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: import(".prisma/client").$Enums.RecurrenceFrequency;
|
||||
daysOfWeek: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
}>;
|
||||
/**
|
||||
* 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;
|
||||
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;
|
||||
date: Date;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
})[];
|
||||
} & {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
frequency: import(".prisma/client").$Enums.RecurrenceFrequency;
|
||||
daysOfWeek: import("@prisma/client/runtime/library").JsonValue | null;
|
||||
}>;
|
||||
/**
|
||||
* Delete series (cascades to shifts via onDelete: SetNull)
|
||||
*/
|
||||
static deleteSeries(seriesId: string): Promise<void>;
|
||||
}
|
||||
//# sourceMappingURL=shift-series.service.d.ts.map
|
||||
1
api/dist/modules/map/shifts/shift-series.service.d.ts.map
vendored
Normal file
1
api/dist/modules/map/shifts/shift-series.service.d.ts.map
vendored
Normal file
@@ -0,0 +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"}
|
||||
180
api/dist/modules/map/shifts/shift-series.service.js
vendored
Normal file
180
api/dist/modules/map/shifts/shift-series.service.js
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ShiftSeriesService = void 0;
|
||||
const database_1 = require("../../../config/database");
|
||||
const client_1 = require("@prisma/client");
|
||||
const MAX_OCCURRENCES = 100;
|
||||
const MAX_DAYS = 365;
|
||||
class ShiftSeriesService {
|
||||
/**
|
||||
* Generate occurrence dates based on recurrence rules
|
||||
*/
|
||||
static generateOccurrences(input) {
|
||||
const dates = [];
|
||||
const startDate = new Date(input.startDate);
|
||||
const endDate = input.endDate ? new Date(input.endDate) : null;
|
||||
let currentDate = new Date(startDate);
|
||||
let count = 0;
|
||||
while (count < MAX_OCCURRENCES) {
|
||||
// Check end date
|
||||
if (endDate && currentDate > endDate)
|
||||
break;
|
||||
// Check max days
|
||||
const daysDiff = Math.floor((currentDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysDiff > MAX_DAYS)
|
||||
break;
|
||||
// Check if date matches recurrence pattern
|
||||
let shouldInclude = false;
|
||||
if (input.frequency === client_1.RecurrenceFrequency.DAILY) {
|
||||
shouldInclude = true;
|
||||
}
|
||||
else if (input.frequency === client_1.RecurrenceFrequency.WEEKLY) {
|
||||
const dayOfWeek = currentDate.getDay(); // 0=Sun, 6=Sat
|
||||
shouldInclude = input.daysOfWeek?.includes(dayOfWeek) ?? false;
|
||||
}
|
||||
else if (input.frequency === client_1.RecurrenceFrequency.MONTHLY) {
|
||||
shouldInclude = currentDate.getDate() === startDate.getDate();
|
||||
}
|
||||
if (shouldInclude) {
|
||||
dates.push(new Date(currentDate));
|
||||
count++;
|
||||
}
|
||||
// Increment date
|
||||
if (input.frequency === client_1.RecurrenceFrequency.DAILY) {
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
else if (input.frequency === client_1.RecurrenceFrequency.WEEKLY) {
|
||||
currentDate.setDate(currentDate.getDate() + 1); // Check each day
|
||||
}
|
||||
else if (input.frequency === client_1.RecurrenceFrequency.MONTHLY) {
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
/**
|
||||
* Create a shift series and generate all individual shifts
|
||||
*/
|
||||
static async createSeries(input, createdBy) {
|
||||
// Generate occurrence dates
|
||||
const dates = this.generateOccurrences(input);
|
||||
if (dates.length === 0) {
|
||||
throw new Error('No valid dates generated from recurrence rules');
|
||||
}
|
||||
// Create series and shifts in transaction
|
||||
const result = await database_1.prisma.$transaction(async (tx) => {
|
||||
// Create series
|
||||
const series = await tx.shiftSeries.create({
|
||||
data: {
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
startTime: input.startTime,
|
||||
endTime: input.endTime,
|
||||
location: input.location,
|
||||
maxVolunteers: input.maxVolunteers,
|
||||
isPublic: input.isPublic ?? false,
|
||||
cutId: input.cutId,
|
||||
frequency: input.frequency,
|
||||
daysOfWeek: input.daysOfWeek,
|
||||
startDate: new Date(input.startDate),
|
||||
endDate: input.endDate ? new Date(input.endDate) : null,
|
||||
createdBy,
|
||||
},
|
||||
});
|
||||
// Create individual shifts
|
||||
const shifts = await Promise.all(dates.map((date) => tx.shift.create({
|
||||
data: {
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
date,
|
||||
startTime: input.startTime,
|
||||
endTime: input.endTime,
|
||||
location: input.location,
|
||||
maxVolunteers: input.maxVolunteers,
|
||||
isPublic: input.isPublic ?? false,
|
||||
cutId: input.cutId,
|
||||
seriesId: series.id,
|
||||
createdBy,
|
||||
},
|
||||
})));
|
||||
return { series, shifts };
|
||||
});
|
||||
return {
|
||||
series: result.series,
|
||||
generatedShiftsCount: result.shifts.length,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get series with all its shifts
|
||||
*/
|
||||
static async getSeries(seriesId) {
|
||||
const series = await database_1.prisma.shiftSeries.findUnique({
|
||||
where: { id: seriesId },
|
||||
include: {
|
||||
cut: true,
|
||||
shifts: {
|
||||
orderBy: { date: 'asc' },
|
||||
include: { signups: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!series) {
|
||||
throw new Error('Shift series not found');
|
||||
}
|
||||
return series;
|
||||
}
|
||||
/**
|
||||
* Update series shifts based on edit mode
|
||||
*/
|
||||
static async updateSeries(seriesId, input) {
|
||||
const series = await database_1.prisma.shiftSeries.findUnique({
|
||||
where: { id: seriesId },
|
||||
include: { shifts: true },
|
||||
});
|
||||
if (!series) {
|
||||
throw new Error('Shift series not found');
|
||||
}
|
||||
const { editMode, fromDate, ...updates } = input;
|
||||
if (editMode === 'ALL') {
|
||||
// Update series + all shifts
|
||||
await database_1.prisma.$transaction(async (tx) => {
|
||||
await tx.shiftSeries.update({
|
||||
where: { id: seriesId },
|
||||
data: updates,
|
||||
});
|
||||
await tx.shift.updateMany({
|
||||
where: { seriesId, isException: false },
|
||||
data: updates,
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (editMode === 'FUTURE' && fromDate) {
|
||||
// Update shifts from date onwards
|
||||
await database_1.prisma.shift.updateMany({
|
||||
where: {
|
||||
seriesId,
|
||||
date: { gte: new Date(fromDate) },
|
||||
isException: false,
|
||||
},
|
||||
data: updates,
|
||||
});
|
||||
}
|
||||
// Note: THIS mode is handled by regular shift update with isException flag
|
||||
return this.getSeries(seriesId);
|
||||
}
|
||||
/**
|
||||
* Delete series (cascades to shifts via onDelete: SetNull)
|
||||
*/
|
||||
static async deleteSeries(seriesId) {
|
||||
// Delete all shifts in series first
|
||||
await database_1.prisma.shift.deleteMany({
|
||||
where: { seriesId },
|
||||
});
|
||||
// Then delete series
|
||||
await database_1.prisma.shiftSeries.delete({
|
||||
where: { id: seriesId },
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.ShiftSeriesService = ShiftSeriesService;
|
||||
//# sourceMappingURL=shift-series.service.js.map
|
||||
1
api/dist/modules/map/shifts/shift-series.service.js.map
vendored
Normal file
1
api/dist/modules/map/shifts/shift-series.service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5
api/dist/modules/map/shifts/shifts.routes.d.ts
vendored
Normal file
5
api/dist/modules/map/shifts/shifts.routes.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare const adminRouter: import("express-serve-static-core").Router;
|
||||
declare const volunteerRouter: import("express-serve-static-core").Router;
|
||||
declare const publicRouter: import("express-serve-static-core").Router;
|
||||
export { adminRouter as shiftsAdminRouter, publicRouter as shiftsPublicRouter, volunteerRouter as shiftsVolunteerRouter };
|
||||
//# sourceMappingURL=shifts.routes.d.ts.map
|
||||
1
api/dist/modules/map/shifts/shifts.routes.d.ts.map
vendored
Normal file
1
api/dist/modules/map/shifts/shifts.routes.d.ts.map
vendored
Normal file
@@ -0,0 +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"}
|
||||
198
api/dist/modules/map/shifts/shifts.routes.js
vendored
Normal file
198
api/dist/modules/map/shifts/shifts.routes.js
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
"use strict";
|
||||
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];
|
||||
// --- 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));
|
||||
// GET /api/map/shifts — list paginated
|
||||
adminRouter.get('/', (0, validate_1.validate)(shifts_schemas_1.listShiftsSchema, 'query'), async (req, res, next) => {
|
||||
try {
|
||||
const result = await shifts_service_1.shiftsService.findAll(req.query);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/map/shifts/stats — statistics
|
||||
adminRouter.get('/stats', async (_req, res, next) => {
|
||||
try {
|
||||
const stats = await shifts_service_1.shiftsService.getStats();
|
||||
res.json(stats);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/map/shifts/calendar — calendar data
|
||||
adminRouter.get('/calendar', async (req, res, next) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({ error: 'startDate and endDate required' });
|
||||
}
|
||||
const data = await shifts_service_1.shiftsService.getCalendarData(startDate, endDate);
|
||||
res.json(data);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/map/shifts/:id — single shift with signups
|
||||
adminRouter.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const shift = await shifts_service_1.shiftsService.findById(id);
|
||||
res.json(shift);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/map/shifts — create
|
||||
adminRouter.post('/', (0, validate_1.validate)(shifts_schemas_1.createShiftSchema), async (req, res, next) => {
|
||||
try {
|
||||
const shift = await shifts_service_1.shiftsService.create(req.body, req.user.id);
|
||||
res.status(201).json(shift);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// PUT /api/map/shifts/:id — update
|
||||
adminRouter.put('/:id', (0, validate_1.validate)(shifts_schemas_1.updateShiftSchema), async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const shift = await shifts_service_1.shiftsService.update(id, req.body);
|
||||
res.json(shift);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// DELETE /api/map/shifts/:id — delete
|
||||
adminRouter.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
await shifts_service_1.shiftsService.delete(id);
|
||||
res.status(204).send();
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/map/shifts/:id/signups — admin add volunteer
|
||||
adminRouter.post('/:id/signups', (0, validate_1.validate)(shifts_schemas_1.addSignupSchema), async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const signup = await shifts_service_1.shiftsService.addSignup(id, req.body);
|
||||
res.status(201).json(signup);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// DELETE /api/map/shifts/:id/signups/:signupId — admin remove volunteer
|
||||
adminRouter.delete('/:id/signups/:signupId', async (req, res, next) => {
|
||||
try {
|
||||
const signupId = req.params.signupId;
|
||||
await shifts_service_1.shiftsService.removeSignup(signupId);
|
||||
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 {
|
||||
const id = req.params.id;
|
||||
const result = await shifts_service_1.shiftsService.emailShiftDetails(id);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- Volunteer Router ---
|
||||
const volunteerRouter = (0, express_1.Router)();
|
||||
exports.shiftsVolunteerRouter = volunteerRouter;
|
||||
volunteerRouter.use(auth_middleware_1.authenticate);
|
||||
// GET /api/map/shifts/volunteer/upcoming — upcoming shifts with signup status
|
||||
volunteerRouter.get('/volunteer/upcoming', async (req, res, next) => {
|
||||
try {
|
||||
const shifts = await shifts_service_1.shiftsService.getUpcomingForVolunteer(req.user.id);
|
||||
res.json(shifts);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// GET /api/map/shifts/volunteer/my-signups — own confirmed signups
|
||||
volunteerRouter.get('/volunteer/my-signups', async (req, res, next) => {
|
||||
try {
|
||||
const signups = await shifts_service_1.shiftsService.getMySignups(req.user.id);
|
||||
res.json(signups);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/map/shifts/volunteer/:id/signup — sign up for shift
|
||||
volunteerRouter.post('/volunteer/:id/signup', rate_limit_1.shiftSignupRateLimit, async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const signup = await shifts_service_1.shiftsService.volunteerSignup(id, req.user.id);
|
||||
res.status(201).json(signup);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// DELETE /api/map/shifts/volunteer/:id/signup — cancel own signup
|
||||
volunteerRouter.delete('/volunteer/:id/signup', async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
await shifts_service_1.shiftsService.cancelVolunteerSignup(id, req.user.id);
|
||||
res.status(204).send();
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// --- Public Router ---
|
||||
const publicRouter = (0, express_1.Router)();
|
||||
exports.shiftsPublicRouter = publicRouter;
|
||||
// GET /api/map/shifts/public — list upcoming public shifts
|
||||
publicRouter.get('/public', async (_req, res, next) => {
|
||||
try {
|
||||
const shifts = await shifts_service_1.shiftsService.getPublicShifts();
|
||||
res.json(shifts);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// POST /api/map/shifts/public/:id/signup — public signup
|
||||
publicRouter.post('/public/:id/signup', rate_limit_1.shiftSignupRateLimit, (0, validate_1.validate)(shifts_schemas_1.publicSignupSchema), async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const result = await shifts_service_1.shiftsService.publicSignup(id, req.body);
|
||||
res.status(201).json(result);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=shifts.routes.js.map
|
||||
1
api/dist/modules/map/shifts/shifts.routes.js.map
vendored
Normal file
1
api/dist/modules/map/shifts/shifts.routes.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
128
api/dist/modules/map/shifts/shifts.schemas.d.ts
vendored
Normal file
128
api/dist/modules/map/shifts/shifts.schemas.d.ts
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
import { z } from 'zod';
|
||||
export declare const createShiftSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
date: z.ZodString;
|
||||
startTime: z.ZodString;
|
||||
endTime: z.ZodString;
|
||||
location: z.ZodOptional<z.ZodString>;
|
||||
maxVolunteers: z.ZodNumber;
|
||||
isPublic: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||
cutId: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
date: string;
|
||||
title: string;
|
||||
isPublic: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
cutId?: string | undefined;
|
||||
}, {
|
||||
date: string;
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
location?: string | undefined;
|
||||
description?: string | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | undefined;
|
||||
}>;
|
||||
export declare const updateShiftSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
date: z.ZodOptional<z.ZodString>;
|
||||
startTime: z.ZodOptional<z.ZodString>;
|
||||
endTime: z.ZodOptional<z.ZodString>;
|
||||
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
maxVolunteers: z.ZodOptional<z.ZodNumber>;
|
||||
isPublic: z.ZodOptional<z.ZodBoolean>;
|
||||
status: z.ZodOptional<z.ZodNativeEnum<{
|
||||
OPEN: "OPEN";
|
||||
FULL: "FULL";
|
||||
CANCELLED: "CANCELLED";
|
||||
}>>;
|
||||
cutId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
date?: string | undefined;
|
||||
location?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | null | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | null | undefined;
|
||||
startTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
maxVolunteers?: number | undefined;
|
||||
}, {
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
date?: string | undefined;
|
||||
location?: string | null | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | null | undefined;
|
||||
isPublic?: boolean | undefined;
|
||||
cutId?: string | null | undefined;
|
||||
startTime?: string | undefined;
|
||||
endTime?: string | undefined;
|
||||
maxVolunteers?: number | undefined;
|
||||
}>;
|
||||
export declare const listShiftsSchema: z.ZodObject<{
|
||||
page: z.ZodDefault<z.ZodNumber>;
|
||||
limit: z.ZodDefault<z.ZodNumber>;
|
||||
search: z.ZodOptional<z.ZodString>;
|
||||
status: z.ZodOptional<z.ZodNativeEnum<{
|
||||
OPEN: "OPEN";
|
||||
FULL: "FULL";
|
||||
CANCELLED: "CANCELLED";
|
||||
}>>;
|
||||
upcoming: z.ZodOptional<z.ZodBoolean>;
|
||||
sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["date", "createdAt", "title"]>>>;
|
||||
sortOrder: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
limit: number;
|
||||
page: number;
|
||||
sortBy: "date" | "createdAt" | "title";
|
||||
sortOrder: "asc" | "desc";
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
search?: string | undefined;
|
||||
upcoming?: boolean | undefined;
|
||||
}, {
|
||||
status?: "OPEN" | "FULL" | "CANCELLED" | undefined;
|
||||
search?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
page?: number | undefined;
|
||||
sortBy?: "date" | "createdAt" | "title" | undefined;
|
||||
sortOrder?: "asc" | "desc" | undefined;
|
||||
upcoming?: boolean | undefined;
|
||||
}>;
|
||||
export declare const addSignupSchema: z.ZodObject<{
|
||||
userEmail: z.ZodString;
|
||||
userName: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
userEmail: string;
|
||||
userName?: string | undefined;
|
||||
}, {
|
||||
userEmail: string;
|
||||
userName?: string | undefined;
|
||||
}>;
|
||||
export declare const publicSignupSchema: z.ZodObject<{
|
||||
email: z.ZodString;
|
||||
name: z.ZodString;
|
||||
phone: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
email: string;
|
||||
name: string;
|
||||
phone?: string | undefined;
|
||||
}, {
|
||||
email: string;
|
||||
name: string;
|
||||
phone?: string | undefined;
|
||||
}>;
|
||||
export type CreateShiftInput = z.infer<typeof createShiftSchema>;
|
||||
export type UpdateShiftInput = z.infer<typeof updateShiftSchema>;
|
||||
export type ListShiftsInput = z.infer<typeof listShiftsSchema>;
|
||||
export type AddSignupInput = z.infer<typeof addSignupSchema>;
|
||||
export type PublicSignupInput = z.infer<typeof publicSignupSchema>;
|
||||
//# sourceMappingURL=shifts.schemas.d.ts.map
|
||||
1
api/dist/modules/map/shifts/shifts.schemas.d.ts.map
vendored
Normal file
1
api/dist/modules/map/shifts/shifts.schemas.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"shifts.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shifts.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU5B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAW5B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQ3B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;EAI7B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC/D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAC7D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
||||
47
api/dist/modules/map/shifts/shifts.schemas.js
vendored
Normal file
47
api/dist/modules/map/shifts/shifts.schemas.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.publicSignupSchema = exports.addSignupSchema = exports.listShiftsSchema = exports.updateShiftSchema = exports.createShiftSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const client_1 = require("@prisma/client");
|
||||
exports.createShiftSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1, 'Title is required'),
|
||||
description: zod_1.z.string().optional(),
|
||||
date: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD'),
|
||||
startTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM'),
|
||||
endTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM'),
|
||||
location: zod_1.z.string().optional(),
|
||||
maxVolunteers: zod_1.z.number().int().min(1, 'Must have at least 1 volunteer spot'),
|
||||
isPublic: zod_1.z.boolean().optional().default(false),
|
||||
cutId: zod_1.z.string().optional(),
|
||||
});
|
||||
exports.updateShiftSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).optional(),
|
||||
description: zod_1.z.string().nullable().optional(),
|
||||
date: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD').optional(),
|
||||
startTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM').optional(),
|
||||
endTime: zod_1.z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM').optional(),
|
||||
location: zod_1.z.string().nullable().optional(),
|
||||
maxVolunteers: zod_1.z.number().int().min(1).optional(),
|
||||
isPublic: zod_1.z.boolean().optional(),
|
||||
status: zod_1.z.nativeEnum(client_1.ShiftStatus).optional(),
|
||||
cutId: zod_1.z.string().nullable().optional(),
|
||||
});
|
||||
exports.listShiftsSchema = zod_1.z.object({
|
||||
page: zod_1.z.coerce.number().int().positive().default(1),
|
||||
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
|
||||
search: zod_1.z.string().optional(),
|
||||
status: zod_1.z.nativeEnum(client_1.ShiftStatus).optional(),
|
||||
upcoming: zod_1.z.coerce.boolean().optional(),
|
||||
sortBy: zod_1.z.enum(['date', 'createdAt', 'title']).optional().default('date'),
|
||||
sortOrder: zod_1.z.enum(['asc', 'desc']).optional().default('desc'),
|
||||
});
|
||||
exports.addSignupSchema = zod_1.z.object({
|
||||
userEmail: zod_1.z.string().email('Valid email is required'),
|
||||
userName: zod_1.z.string().optional(),
|
||||
});
|
||||
exports.publicSignupSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email('Valid email is required'),
|
||||
name: zod_1.z.string().min(1, 'Name is required'),
|
||||
phone: zod_1.z.string().optional(),
|
||||
});
|
||||
//# sourceMappingURL=shifts.schemas.js.map
|
||||
1
api/dist/modules/map/shifts/shifts.schemas.js.map
vendored
Normal file
1
api/dist/modules/map/shifts/shifts.schemas.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"shifts.schemas.js","sourceRoot":"","sources":["../../../../src/modules/map/shifts/shifts.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,2CAA6C;AAEhC,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,mBAAmB,CAAC;IAC7C,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,EAAE,yBAAyB,CAAC;IACxE,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,0BAA0B,CAAC;IACxE,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,wBAAwB,CAAC;IACpE,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,qCAAqC,CAAC;IAC7E,QAAQ,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC/C,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC,CAAC;AAEU,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnC,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC7C,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,EAAE,yBAAyB,CAAC,CAAC,QAAQ,EAAE;IACnF,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC,QAAQ,EAAE;IACnF,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,wBAAwB,CAAC,CAAC,QAAQ,EAAE;IAC/E,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC1C,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACjD,QAAQ,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,oBAAW,CAAC,CAAC,QAAQ,EAAE;IAC5C,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAEU,QAAA,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,oBAAW,CAAC,CAAC,QAAQ,EAAE;IAC5C,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IACvC,MAAM,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;IACzE,SAAS,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;CAC9D,CAAC,CAAC;AAEU,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC;IACtD,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC,CAAC;AAEU,QAAA,kBAAkB,GAAG,OAAC,CAAC,MAAM,CAAC;IACzC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC;IAClD,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC;IAC3C,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC,CAAC"}
|
||||
253
api/dist/modules/map/shifts/shifts.service.d.ts
vendored
Normal file
253
api/dist/modules/map/shifts/shifts.service.d.ts
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { CreateShiftInput, UpdateShiftInput, ListShiftsInput, AddSignupInput, PublicSignupInput } from './shifts.schemas';
|
||||
export declare const shiftsService: {
|
||||
findAll(filters: ListShiftsInput): Promise<{
|
||||
shifts: ({
|
||||
_count: {
|
||||
signups: number;
|
||||
};
|
||||
cut: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
date: Date;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
})[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}>;
|
||||
findById(id: string): Promise<{
|
||||
_count: {
|
||||
signups: number;
|
||||
};
|
||||
cut: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
signups: ({
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
phone: string | null;
|
||||
} | null;
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: 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;
|
||||
date: Date;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
}>;
|
||||
create(data: CreateShiftInput, userId: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
date: Date;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
}>;
|
||||
update(id: string, data: UpdateShiftInput): Promise<{
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
date: Date;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
isPublic: boolean;
|
||||
cutId: string | null;
|
||||
createdBy: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
seriesId: string | null;
|
||||
isException: boolean;
|
||||
}>;
|
||||
delete(id: string): Promise<void>;
|
||||
getStats(): Promise<{
|
||||
total: number;
|
||||
open: number;
|
||||
full: number;
|
||||
cancelled: number;
|
||||
upcoming: number;
|
||||
totalSignups: number;
|
||||
}>;
|
||||
getSignups(shiftId: string): Promise<({
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
phone: string | null;
|
||||
} | null;
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: 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;
|
||||
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<{
|
||||
signup: {
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: string;
|
||||
userName: string | null;
|
||||
shiftTitle: string | null;
|
||||
userPhone: string | null;
|
||||
signupDate: Date;
|
||||
signupSource: import(".prisma/client").$Enums.SignupSource;
|
||||
shiftId: string;
|
||||
};
|
||||
isNewUser: boolean;
|
||||
}>;
|
||||
cancelPublicSignup(shiftId: string, userEmail: string): Promise<void>;
|
||||
getUpcomingForVolunteer(userId: string): Promise<{
|
||||
isSignedUp: boolean;
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
date: Date;
|
||||
id: string;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
}[]>;
|
||||
volunteerSignup(shiftId: string, userId: string): Promise<{
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: 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<({
|
||||
shift: {
|
||||
status: import(".prisma/client").$Enums.ShiftStatus;
|
||||
date: Date;
|
||||
id: string;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
};
|
||||
} & {
|
||||
status: import(".prisma/client").$Enums.SignupStatus;
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userEmail: 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;
|
||||
date: Date;
|
||||
id: string;
|
||||
location: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
maxVolunteers: number;
|
||||
currentVolunteers: number;
|
||||
}[]>;
|
||||
emailShiftDetails(shiftId: string): Promise<{
|
||||
sent: number;
|
||||
failed: number;
|
||||
}>;
|
||||
getCalendarData(startDate: string, endDate: string): Promise<{
|
||||
dates: Record<string, {
|
||||
count: number;
|
||||
shifts: any[];
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
//# sourceMappingURL=shifts.service.d.ts.map
|
||||
1
api/dist/modules/map/shifts/shifts.service.d.ts.map
vendored
Normal file
1
api/dist/modules/map/shifts/shifts.service.d.ts.map
vendored
Normal file
@@ -0,0 +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"}
|
||||
714
api/dist/modules/map/shifts/shifts.service.js
vendored
Normal file
714
api/dist/modules/map/shifts/shifts.service.js
vendored
Normal file
@@ -0,0 +1,714 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.shiftsService = void 0;
|
||||
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||
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 env_1 = require("../../../config/env");
|
||||
const logger_1 = require("../../../utils/logger");
|
||||
const metrics_1 = require("../../../utils/metrics");
|
||||
const adjectives = ['Blue', 'Red', 'Green', 'Swift', 'Bright', 'Bold', 'Calm', 'Fair'];
|
||||
const nouns = ['Eagle', 'River', 'Mountain', 'Star', 'Forest', 'Lake', 'Wolf', 'Hawk'];
|
||||
function generateReadablePassword() {
|
||||
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
const num = Math.floor(Math.random() * 90) + 10;
|
||||
return `${adj}${noun}${num}`;
|
||||
}
|
||||
exports.shiftsService = {
|
||||
async findAll(filters) {
|
||||
const { page, limit, search, status, upcoming, sortBy, sortOrder } = filters;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = {};
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ location: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
if (status)
|
||||
where.status = status;
|
||||
if (upcoming) {
|
||||
where.date = { gte: new Date() };
|
||||
}
|
||||
const orderBy = sortBy === 'title'
|
||||
? { title: sortOrder }
|
||||
: sortBy === 'createdAt'
|
||||
? { createdAt: sortOrder }
|
||||
: { date: sortOrder };
|
||||
const [shifts, total] = await Promise.all([
|
||||
database_1.prisma.shift.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy,
|
||||
include: {
|
||||
cut: { select: { id: true, name: true } },
|
||||
_count: {
|
||||
select: {
|
||||
signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
database_1.prisma.shift.count({ where }),
|
||||
]);
|
||||
return {
|
||||
shifts,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
async findById(id) {
|
||||
const shift = await database_1.prisma.shift.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
cut: { select: { id: true, name: true } },
|
||||
signups: {
|
||||
where: { status: client_1.SignupStatus.CONFIRMED },
|
||||
include: { user: { select: { id: true, email: true, name: true, phone: true } } },
|
||||
orderBy: { signupDate: 'desc' },
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!shift) {
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
}
|
||||
return shift;
|
||||
},
|
||||
async create(data, userId) {
|
||||
const shift = await database_1.prisma.shift.create({
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
date: new Date(data.date),
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
location: data.location,
|
||||
maxVolunteers: data.maxVolunteers,
|
||||
isPublic: data.isPublic,
|
||||
cutId: data.cutId,
|
||||
createdBy: userId,
|
||||
},
|
||||
});
|
||||
return shift;
|
||||
},
|
||||
async update(id, data) {
|
||||
const existing = await database_1.prisma.shift.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
}
|
||||
const updateData = { ...data };
|
||||
if (data.date) {
|
||||
updateData.date = new Date(data.date);
|
||||
}
|
||||
// Auto-update status when capacity changes
|
||||
if (data.maxVolunteers !== undefined) {
|
||||
if (existing.currentVolunteers >= data.maxVolunteers && existing.status === client_1.ShiftStatus.OPEN) {
|
||||
updateData.status = client_1.ShiftStatus.FULL;
|
||||
}
|
||||
else if (existing.currentVolunteers < data.maxVolunteers && existing.status === client_1.ShiftStatus.FULL) {
|
||||
updateData.status = client_1.ShiftStatus.OPEN;
|
||||
}
|
||||
}
|
||||
const shift = await database_1.prisma.shift.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
return shift;
|
||||
},
|
||||
async delete(id) {
|
||||
const existing = await database_1.prisma.shift.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
}
|
||||
await database_1.prisma.shift.delete({ where: { id } });
|
||||
},
|
||||
async getStats() {
|
||||
const [total, open, full, cancelled, upcoming, totalSignups] = await Promise.all([
|
||||
database_1.prisma.shift.count(),
|
||||
database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.OPEN } }),
|
||||
database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.FULL } }),
|
||||
database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.CANCELLED } }),
|
||||
database_1.prisma.shift.count({ where: { date: { gte: new Date() }, status: { not: client_1.ShiftStatus.CANCELLED } } }),
|
||||
database_1.prisma.shiftSignup.count({ where: { status: client_1.SignupStatus.CONFIRMED } }),
|
||||
]);
|
||||
return { total, open, full, cancelled, upcoming, totalSignups };
|
||||
},
|
||||
async getSignups(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');
|
||||
}
|
||||
return database_1.prisma.shiftSignup.findMany({
|
||||
where: { shiftId },
|
||||
include: { user: { select: { id: true, email: true, name: true, phone: true } } },
|
||||
orderBy: { signupDate: 'desc' },
|
||||
});
|
||||
},
|
||||
async addSignup(shiftId, data) {
|
||||
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.currentVolunteers >= shift.maxVolunteers) {
|
||||
throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
|
||||
}
|
||||
// Check unique constraint
|
||||
const existing = await database_1.prisma.shiftSignup.findUnique({
|
||||
where: { shiftId_userEmail: { shiftId, userEmail: data.userEmail } },
|
||||
});
|
||||
if (existing) {
|
||||
if (existing.status === client_1.SignupStatus.CANCELLED) {
|
||||
// Re-activate cancelled signup
|
||||
const [signup] = await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: existing.id },
|
||||
data: { status: client_1.SignupStatus.CONFIRMED, signupSource: client_1.SignupSource.ADMIN },
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { increment: 1 },
|
||||
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return signup;
|
||||
}
|
||||
throw new error_handler_1.AppError(409, 'Volunteer already signed up', 'DUPLICATE_SIGNUP');
|
||||
}
|
||||
// Look up user
|
||||
const user = await database_1.prisma.user.findUnique({ where: { email: data.userEmail } });
|
||||
const [signup] = await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.create({
|
||||
data: {
|
||||
shiftId,
|
||||
shiftTitle: shift.title,
|
||||
userId: user?.id,
|
||||
userEmail: data.userEmail,
|
||||
userName: data.userName || user?.name,
|
||||
signupSource: client_1.SignupSource.ADMIN,
|
||||
},
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { increment: 1 },
|
||||
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return signup;
|
||||
},
|
||||
async removeSignup(signupId) {
|
||||
const signup = await database_1.prisma.shiftSignup.findUnique({ where: { id: signupId } });
|
||||
if (!signup) {
|
||||
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, 'Signup already cancelled', 'ALREADY_CANCELLED');
|
||||
}
|
||||
await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: signupId },
|
||||
data: { status: client_1.SignupStatus.CANCELLED },
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: signup.shiftId },
|
||||
data: {
|
||||
currentVolunteers: { decrement: 1 },
|
||||
status: client_1.ShiftStatus.OPEN,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
async publicSignup(shiftId, data) {
|
||||
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.isPublic) {
|
||||
throw new error_handler_1.AppError(403, 'This shift is not open for public signup', 'NOT_PUBLIC');
|
||||
}
|
||||
if (shift.status !== client_1.ShiftStatus.OPEN) {
|
||||
throw new error_handler_1.AppError(400, 'This shift is not accepting signups', 'NOT_OPEN');
|
||||
}
|
||||
if (shift.date < new Date(new Date().toISOString().split('T')[0])) {
|
||||
throw new error_handler_1.AppError(400, 'This shift has already passed', 'SHIFT_PAST');
|
||||
}
|
||||
if (shift.currentVolunteers >= shift.maxVolunteers) {
|
||||
throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
|
||||
}
|
||||
// Check unique constraint
|
||||
const existingSignup = await database_1.prisma.shiftSignup.findUnique({
|
||||
where: { shiftId_userEmail: { shiftId, userEmail: data.email } },
|
||||
});
|
||||
if (existingSignup && existingSignup.status === client_1.SignupStatus.CONFIRMED) {
|
||||
throw new error_handler_1.AppError(409, 'You are already signed up for this shift', 'DUPLICATE_SIGNUP');
|
||||
}
|
||||
// Look up existing user
|
||||
let user = await database_1.prisma.user.findUnique({ where: { email: data.email } });
|
||||
let isNewUser = false;
|
||||
let tempPassword;
|
||||
if (!user) {
|
||||
// Create temp user
|
||||
tempPassword = generateReadablePassword();
|
||||
const hashedPassword = await bcryptjs_1.default.hash(tempPassword, 12);
|
||||
const shiftDate = new Date(shift.date);
|
||||
shiftDate.setDate(shiftDate.getDate() + 1);
|
||||
user = await database_1.prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
role: 'TEMP',
|
||||
createdVia: 'PUBLIC_SHIFT_SIGNUP',
|
||||
expiresAt: shiftDate,
|
||||
},
|
||||
});
|
||||
isNewUser = true;
|
||||
}
|
||||
// Create signup (or re-activate cancelled one)
|
||||
let signup;
|
||||
if (existingSignup && existingSignup.status === client_1.SignupStatus.CANCELLED) {
|
||||
[signup] = await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: existingSignup.id },
|
||||
data: {
|
||||
status: client_1.SignupStatus.CONFIRMED,
|
||||
signupSource: user ? client_1.SignupSource.AUTHENTICATED : client_1.SignupSource.PUBLIC,
|
||||
userName: data.name,
|
||||
userPhone: data.phone,
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { increment: 1 },
|
||||
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
else {
|
||||
[signup] = await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.create({
|
||||
data: {
|
||||
shiftId,
|
||||
shiftTitle: shift.title,
|
||||
userId: user.id,
|
||||
userEmail: data.email,
|
||||
userName: data.name,
|
||||
userPhone: data.phone,
|
||||
signupSource: isNewUser ? client_1.SignupSource.PUBLIC : client_1.SignupSource.AUTHENTICATED,
|
||||
},
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { increment: 1 },
|
||||
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
// Send confirmation email
|
||||
try {
|
||||
const shiftDate = new Date(shift.date);
|
||||
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 { /* 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,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to send shift signup confirmation email:', err);
|
||||
}
|
||||
(0, metrics_1.recordShiftSignup)();
|
||||
return { signup, isNewUser };
|
||||
},
|
||||
async cancelPublicSignup(shiftId, userEmail) {
|
||||
const signup = await database_1.prisma.shiftSignup.findUnique({
|
||||
where: { shiftId_userEmail: { shiftId, userEmail } },
|
||||
});
|
||||
if (!signup) {
|
||||
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, 'Signup already cancelled', 'ALREADY_CANCELLED');
|
||||
}
|
||||
await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: signup.id },
|
||||
data: { status: client_1.SignupStatus.CANCELLED },
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { decrement: 1 },
|
||||
status: client_1.ShiftStatus.OPEN,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
async getUpcomingForVolunteer(userId) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
||||
if (!user)
|
||||
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
||||
const shifts = await database_1.prisma.shift.findMany({
|
||||
where: {
|
||||
isPublic: true,
|
||||
status: { not: client_1.ShiftStatus.CANCELLED },
|
||||
date: { gte: new Date(new Date().toISOString().split('T')[0]) },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
maxVolunteers: true,
|
||||
currentVolunteers: true,
|
||||
status: true,
|
||||
},
|
||||
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
// Check signup status for each shift
|
||||
const signups = await database_1.prisma.shiftSignup.findMany({
|
||||
where: {
|
||||
userEmail: user.email,
|
||||
shiftId: { in: shifts.map((s) => s.id) },
|
||||
status: client_1.SignupStatus.CONFIRMED,
|
||||
},
|
||||
select: { shiftId: true },
|
||||
});
|
||||
const signedUpSet = new Set(signups.map((s) => s.shiftId));
|
||||
return shifts.map((s) => ({
|
||||
...s,
|
||||
isSignedUp: signedUpSet.has(s.id),
|
||||
}));
|
||||
},
|
||||
async volunteerSignup(shiftId, userId) {
|
||||
const user = await database_1.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, email: true, name: true, phone: true },
|
||||
});
|
||||
if (!user)
|
||||
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
||||
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.isPublic)
|
||||
throw new error_handler_1.AppError(403, 'This shift is not open for signup', 'NOT_PUBLIC');
|
||||
if (shift.status === client_1.ShiftStatus.CANCELLED)
|
||||
throw new error_handler_1.AppError(400, 'This shift is cancelled', 'SHIFT_CANCELLED');
|
||||
if (shift.date < new Date(new Date().toISOString().split('T')[0]))
|
||||
throw new error_handler_1.AppError(400, 'This shift has already passed', 'SHIFT_PAST');
|
||||
if (shift.currentVolunteers >= shift.maxVolunteers)
|
||||
throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
|
||||
const existing = await database_1.prisma.shiftSignup.findUnique({
|
||||
where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
|
||||
});
|
||||
if (existing && existing.status === client_1.SignupStatus.CONFIRMED) {
|
||||
throw new error_handler_1.AppError(409, 'Already signed up for this shift', 'DUPLICATE_SIGNUP');
|
||||
}
|
||||
let signup;
|
||||
if (existing && existing.status === client_1.SignupStatus.CANCELLED) {
|
||||
[signup] = await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: existing.id },
|
||||
data: { status: client_1.SignupStatus.CONFIRMED, signupSource: client_1.SignupSource.AUTHENTICATED },
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { increment: 1 },
|
||||
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
else {
|
||||
[signup] = await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.create({
|
||||
data: {
|
||||
shiftId,
|
||||
shiftTitle: shift.title,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
userName: user.name,
|
||||
userPhone: user.phone,
|
||||
signupSource: client_1.SignupSource.AUTHENTICATED,
|
||||
},
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { increment: 1 },
|
||||
status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
// Send confirmation email
|
||||
try {
|
||||
const shiftDate = new Date(shift.date);
|
||||
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,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error('Failed to send volunteer shift signup confirmation email:', err);
|
||||
}
|
||||
return signup;
|
||||
},
|
||||
async cancelVolunteerSignup(shiftId, userId) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
||||
if (!user)
|
||||
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
||||
const signup = await database_1.prisma.shiftSignup.findUnique({
|
||||
where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
|
||||
});
|
||||
if (!signup)
|
||||
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');
|
||||
await database_1.prisma.$transaction([
|
||||
database_1.prisma.shiftSignup.update({
|
||||
where: { id: signup.id },
|
||||
data: { status: client_1.SignupStatus.CANCELLED },
|
||||
}),
|
||||
database_1.prisma.shift.update({
|
||||
where: { id: shiftId },
|
||||
data: {
|
||||
currentVolunteers: { decrement: 1 },
|
||||
status: client_1.ShiftStatus.OPEN,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
async getMySignups(userId) {
|
||||
const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
|
||||
if (!user)
|
||||
throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
|
||||
const signups = await database_1.prisma.shiftSignup.findMany({
|
||||
where: {
|
||||
userEmail: user.email,
|
||||
status: client_1.SignupStatus.CONFIRMED,
|
||||
shift: {
|
||||
date: { gte: new Date(new Date().toISOString().split('T')[0]) },
|
||||
status: { not: client_1.ShiftStatus.CANCELLED },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
shift: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
maxVolunteers: true,
|
||||
currentVolunteers: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { shift: { date: 'asc' } },
|
||||
});
|
||||
return signups;
|
||||
},
|
||||
async getPublicShifts() {
|
||||
const shifts = await database_1.prisma.shift.findMany({
|
||||
where: {
|
||||
isPublic: true,
|
||||
status: { not: client_1.ShiftStatus.CANCELLED },
|
||||
date: { gte: new Date(new Date().toISOString().split('T')[0]) },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
maxVolunteers: true,
|
||||
currentVolunteers: true,
|
||||
status: true,
|
||||
},
|
||||
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
return shifts;
|
||||
},
|
||||
async emailShiftDetails(shiftId) {
|
||||
const shift = await database_1.prisma.shift.findUnique({
|
||||
where: { id: shiftId },
|
||||
include: {
|
||||
signups: {
|
||||
where: { status: client_1.SignupStatus.CONFIRMED },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!shift) {
|
||||
throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
|
||||
}
|
||||
const shiftDate = new Date(shift.date);
|
||||
const dateStr = shiftDate.toLocaleDateString('en-CA', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
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,
|
||||
});
|
||||
if (result.success) {
|
||||
sent++;
|
||||
}
|
||||
else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.logger.error(`Failed to send shift details to ${signup.userEmail}:`, err);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
return { sent, failed };
|
||||
},
|
||||
async getCalendarData(startDate, endDate) {
|
||||
const shifts = await database_1.prisma.shift.findMany({
|
||||
where: {
|
||||
date: {
|
||||
gte: new Date(startDate),
|
||||
lte: new Date(endDate),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
cut: { select: { id: true, name: true } },
|
||||
signups: true,
|
||||
series: { select: { id: true, frequency: true } },
|
||||
},
|
||||
orderBy: { startTime: 'asc' },
|
||||
});
|
||||
// Group by date
|
||||
const dateMap = {};
|
||||
for (const shift of shifts) {
|
||||
const dateKey = shift.date.toISOString().split('T')[0];
|
||||
if (!dateMap[dateKey]) {
|
||||
dateMap[dateKey] = { count: 0, shifts: [] };
|
||||
}
|
||||
dateMap[dateKey].count++;
|
||||
dateMap[dateKey].shifts.push(shift);
|
||||
}
|
||||
return { dates: dateMap };
|
||||
},
|
||||
};
|
||||
//# sourceMappingURL=shifts.service.js.map
|
||||
1
api/dist/modules/map/shifts/shifts.service.js.map
vendored
Normal file
1
api/dist/modules/map/shifts/shifts.service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user