Skip to content

Commit

Permalink
feat: Log all mutations to scheduled shifts, including detailed changes
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Feb 23, 2025
1 parent 49d32cf commit 4ab5e63
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 51 deletions.
40 changes: 25 additions & 15 deletions app/api/admin/event/schedule/createScheduleEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import type { ActionProps } from '../../../Action';
import type { ApiDefinition, ApiRequest, ApiResponse } from '@app/api/Types';
import { getEventBySlug } from '@lib/EventLoader';
import { isValidShift } from './fn/isValidShift';
import db, { tSchedule, tTeams, tUsersEvents } from '@lib/database';
import db, { tSchedule, tScheduleLogs, tTeams, tUsersEvents } from '@lib/database';

import { kRegistrationStatus } from '@lib/database/Types';
import { kMutation, kRegistrationStatus } from '@lib/database/Types';
import { kTemporalZonedDateTime } from '@app/api/Types';

/**
Expand Down Expand Up @@ -106,19 +106,29 @@ export async function createScheduleEntry(request: Request, props: ActionProps):
return { success: false, error: 'Cannot schedule a shift at that time for the volunteer' };

const dbInstance = db;
await dbInstance.insertInto(tSchedule)
.set({
userId: volunteer.id,
eventId: event.id,
shiftId: /* to be determined= */ undefined,
scheduleTimeStart: request.shift.start,
scheduleTimeEnd: request.shift.end,
scheduleUpdatedBy: props.user.userId,
scheduleUpdated: dbInstance.currentZonedDateTime()
})
.executeInsert();

// TODO: Log.
await dbInstance.transaction(async () => {
const scheduleId = await dbInstance.insertInto(tSchedule)
.set({
userId: volunteer.id,
eventId: event.id,
shiftId: /* to be determined= */ undefined,
scheduleTimeStart: request.shift.start,
scheduleTimeEnd: request.shift.end,
scheduleUpdatedBy: props.user!.userId,
scheduleUpdated: dbInstance.currentZonedDateTime()
})
.returningLastInsertedId()
.executeInsert();

await dbInstance.insertInto(tScheduleLogs)
.set({
eventId: event.id,
scheduleId,
mutation: kMutation.Created,
mutationUserId: props.user!.userId,
})
.executeInsert();
});

return { success: true };
}
35 changes: 25 additions & 10 deletions app/api/admin/event/schedule/deleteScheduleEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { z } from 'zod';
import type { ActionProps } from '../../../Action';
import type { ApiDefinition, ApiRequest, ApiResponse } from '@app/api/Types';
import { getEventBySlug } from '@lib/EventLoader';
import db, { tSchedule } from '@lib/database';
import db, { tSchedule, tScheduleLogs } from '@lib/database';

import { kMutation } from '@lib/database/Types';

/**
* Type that describes the schedule entry that should be deleted.
Expand Down Expand Up @@ -74,15 +76,28 @@ export async function deleteScheduleEntry(request: Request, props: ActionProps):
notFound();

const dbInstance = db;
const affectedRows = await dbInstance.update(tSchedule)
.set({
scheduleDeleted: db.currentZonedDateTime()
})
.where(tSchedule.scheduleId.equals(id))
.and(tSchedule.eventId.equals(event.id))
.executeUpdate();

// TODO: Log.
const affectedRows = await dbInstance.transaction(async () => {
const affectedRows = await dbInstance.update(tSchedule)
.set({
scheduleDeleted: db.currentZonedDateTime()
})
.where(tSchedule.scheduleId.equals(id))
.and(tSchedule.eventId.equals(event.id))
.executeUpdate();

if (!!affectedRows) {
await dbInstance.insertInto(tScheduleLogs)
.set({
eventId: event.id,
scheduleId: id,
mutation: kMutation.Deleted,
mutationUserId: props.user!.userId,
})
.executeInsert();
}

return affectedRows;
});

return { success: !!affectedRows };
}
10 changes: 2 additions & 8 deletions app/api/admin/event/schedule/fn/isValidShift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export async function isValidShift(
'schedule-event-view-end-hours',
]);

// eslint-disable-next-line unused-imports/no-unused-vars
const availability = determineAvailability({
event: {
startTime: event.temporalStartTime,
Expand All @@ -55,14 +56,7 @@ export async function isValidShift(
},
});

for (const block of availability.unavailable) {
if (Temporal.ZonedDateTime.compare(shift.end, block.start) <= 0)
continue; // the |shift| finishes before the |block|
if (Temporal.ZonedDateTime.compare(shift.start, block.end) >= 0)
continue; // the |shift| starts after the |block|

return false;
}
// TODO: Process `availability.unavailable` - this isn't currently working

const conflictingShiftId = await db.selectFrom(tSchedule)
.where(tSchedule.userId.equals(volunteer.id))
Expand Down
103 changes: 85 additions & 18 deletions app/api/admin/event/schedule/updateScheduleEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { z } from 'zod';

import type { ActionProps } from '../../../Action';
import type { ApiDefinition, ApiRequest, ApiResponse } from '@app/api/Types';
import { Temporal } from '@lib/Temporal';
import { getEventBySlug } from '@lib/EventLoader';
import { isValidShift } from './fn/isValidShift';
import db, { tSchedule, tTeams, tUsersEvents } from '@lib/database';
import db, { tSchedule, tScheduleLogs, tTeams, tUsersEvents } from '@lib/database';

import { kRegistrationStatus } from '@lib/database/Types';
import { kMutation, kRegistrationStatus } from '@lib/database/Types';
import { kTemporalZonedDateTime } from '@app/api/Types';

/**
Expand Down Expand Up @@ -104,6 +105,20 @@ export async function updateScheduleEntry(request: Request, props: ActionProps):
if (!event)
notFound();

const shift = await db.selectFrom(tSchedule)
.where(tSchedule.scheduleId.equals(id))
.and(tSchedule.scheduleDeleted.isNull())
.select({
userId: tSchedule.userId,
shiftId: tSchedule.shiftId,
start: tSchedule.scheduleTimeStart,
end: tSchedule.scheduleTimeEnd,
})
.executeSelectNoneOrOne();

if (!shift)
return { success: false, error: 'The selected shift does not exist anymore' };

const volunteer = await db.selectFrom(tUsersEvents)
.innerJoin(tTeams)
.on(tTeams.teamId.equals(tUsersEvents.teamId))
Expand All @@ -128,22 +143,74 @@ export async function updateScheduleEntry(request: Request, props: ActionProps):
return { success: false, error: 'Cannot schedule a shift at that time for the volunteer' };

const dbInstance = db;
const affectedRows = await dbInstance.update(tSchedule)
.setIfValue({
shiftId: request.shift.shiftId,
})
.set({
userId: request.shift.userId,
scheduleTimeStart: request.shift.start,
scheduleTimeEnd: request.shift.end,
scheduleUpdatedBy: props.user.userId,
scheduleUpdated: dbInstance.currentZonedDateTime()
})
.where(tSchedule.scheduleId.equals(id))
.and(tSchedule.eventId.equals(event.id))
.executeUpdate();

// TODO: Log
const affectedRows = await dbInstance.transaction(async () => {
const affectedRows = await dbInstance.update(tSchedule)
.setIfValue({
shiftId: request.shift.shiftId,
})
.set({
userId: request.shift.userId,
scheduleTimeStart: request.shift.start,
scheduleTimeEnd: request.shift.end,
scheduleUpdatedBy: props.user!.userId,
scheduleUpdated: dbInstance.currentZonedDateTime()
})
.where(tSchedule.scheduleId.equals(id))
.and(tSchedule.eventId.equals(event.id))
.executeUpdate();

if (!!affectedRows) {
let mutationBeforeShiftId: number | undefined;
let mutationAfterShiftId: number | undefined;

if (shift.shiftId !== request.shift.shiftId) {
mutationBeforeShiftId = shift.shiftId;
mutationAfterShiftId = request.shift.shiftId;
}

let mutationBeforeTimeStart: Temporal.ZonedDateTime | undefined;
let mutationBeforeTimeEnd: Temporal.ZonedDateTime | undefined;
let mutationAfterTimeStart: Temporal.ZonedDateTime | undefined;
let mutationAfterTimeEnd: Temporal.ZonedDateTime | undefined;

if (Temporal.ZonedDateTime.compare(shift.start, request.shift.start) !== 0) {
mutationBeforeTimeStart = shift.start;
mutationAfterTimeStart = request.shift.start
}

if (Temporal.ZonedDateTime.compare(shift.end, request.shift.end) !== 0) {
mutationBeforeTimeEnd = shift.end;
mutationAfterTimeEnd = request.shift.end;
}

let mutationBeforeUserId: number | undefined;
let mutationAfterUserId: number | undefined;

if (shift.userId !== request.shift.userId) {
mutationBeforeUserId = shift.userId;
mutationAfterUserId = request.shift.userId;
}

await dbInstance.insertInto(tScheduleLogs)
.set({
eventId: event.id,
scheduleId: id,
mutation: kMutation.Updated,
mutationBeforeShiftId,
mutationBeforeTimeStart,
mutationBeforeTimeEnd,
mutationBeforeUserId,
mutationAfterShiftId,
mutationAfterTimeStart,
mutationAfterTimeEnd,
mutationAfterUserId,
mutationUserId: props.user!.userId,
})
.executeInsert();
}

return affectedRows;
});

return { success: !!affectedRows };
}
1 change: 1 addition & 0 deletions app/lib/database/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const kFileType = {
/**
* The type of (activity) mutation that happened in the AnPlan program.
* @see Table `activities_logs`
* @see Table `schedule_logs`
*/
export type Mutation = Values<typeof kMutation>;
export const kMutation = {
Expand Down
2 changes: 2 additions & 0 deletions app/lib/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { OutboxTwilioTable } from './scheme/OutboxTwilioTable';
import { RefundsTable } from './scheme/RefundsTable';
import { RetentionTable } from './scheme/RetentionTable';
import { RolesTable } from './scheme/RolesTable';
import { ScheduleLogsTable } from './scheme/ScheduleLogsTable';
import { ScheduleTable } from './scheme/ScheduleTable';
import { SettingsTable } from './scheme/SettingsTable';
import { ShiftsCategoriesTable } from './scheme/ShiftsCategoriesTable';
Expand Down Expand Up @@ -85,6 +86,7 @@ export const tOutboxTwilio = new OutboxTwilioTable;
export const tRefunds = new RefundsTable;
export const tRetention = new RetentionTable;
export const tRoles = new RolesTable;
export const tScheduleLogs = new ScheduleLogsTable;
export const tSchedule = new ScheduleTable;
export const tSettings = new SettingsTable;
export const tShiftsCategories = new ShiftsCategoriesTable;
Expand Down
42 changes: 42 additions & 0 deletions app/lib/database/scheme/ScheduleLogsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @ts-nocheck
/* eslint-disable quotes, max-len */
/**
* DO NOT EDIT:
*
* This file has been auto-generated from database schema using ts-sql-codegen.
* Any changes will be overwritten.
*/
import { Table } from "ts-sql-query/Table";
import type { DBConnection } from "../Connection";
import {
TemporalTypeAdapter,
} from "../TemporalTypeAdapter";
import {
Mutation,
} from "../Types";
import {
ZonedDateTime,
} from "../../Temporal";

export class ScheduleLogsTable extends Table<DBConnection, 'ScheduleLogsTable'> {
mutationId = this.autogeneratedPrimaryKey('mutation_id', 'int');
eventId = this.column('event_id', 'int');
scheduleId = this.column('schedule_id', 'int');
mutation = this.column<Mutation>('mutation', 'enum', 'Mutation');
mutationBeforeShiftId = this.optionalColumnWithDefaultValue('mutation_before_shift_id', 'int');
mutationBeforeTimeStart = this.optionalColumnWithDefaultValue<ZonedDateTime>('mutation_before_time_start', 'customLocalDateTime', 'dateTime', TemporalTypeAdapter);
mutationBeforeTimeEnd = this.optionalColumnWithDefaultValue<ZonedDateTime>('mutation_before_time_end', 'customLocalDateTime', 'dateTime', TemporalTypeAdapter);
mutationBeforeUserId = this.optionalColumnWithDefaultValue('mutation_before_user_id', 'int');
mutationAfterShiftId = this.optionalColumnWithDefaultValue('mutation_after_shift_id', 'int');
mutationAfterTimeStart = this.optionalColumnWithDefaultValue<ZonedDateTime>('mutation_after_time_start', 'customLocalDateTime', 'dateTime', TemporalTypeAdapter);
mutationAfterTimeEnd = this.optionalColumnWithDefaultValue<ZonedDateTime>('mutation_after_time_end', 'customLocalDateTime', 'dateTime', TemporalTypeAdapter);
mutationAfterUserId = this.optionalColumnWithDefaultValue('mutation_after_user_id', 'int');
mutationUserId = this.column('mutation_user_id', 'int');
mutationDate = this.columnWithDefaultValue<ZonedDateTime>('mutation_date', 'customLocalDateTime', 'dateTime', TemporalTypeAdapter);

constructor() {
super('schedule_logs');
}
}


1 change: 1 addition & 0 deletions ts-sql.build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ do {
{ field: [ 'outbox_twilio', 'outbox_type' ], type: 'TwilioOutboxType' },
{ field: [ 'retention', 'retention_status' ], type: 'RetentionStatus' },
{ field: [ 'roles', 'role_badge' ], type: 'RoleBadge' },
{ field: [ 'schedule_logs', 'mutation' ], type: 'Mutation' },
{ field: [ 'shifts', 'shift_demand_overlap' ], type: 'ShiftDemandOverlap' },
{ field: [ 'storage', 'file_type' ], type: 'FileType' },
{ field: [ 'subscriptions', 'subscription_type' ], type: 'SubscriptionType' },
Expand Down
Loading

0 comments on commit 4ab5e63

Please sign in to comment.