Skip to content

Commit

Permalink
Introduce notes on the volunteer pages
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed May 5, 2024
1 parent e334866 commit 362deb2
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2024 Peter Beverloo & AnimeCon. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

'use client';

import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';

import { FormContainer, TextareaAutosizeElement, type FieldValues } from 'react-hook-form-mui';

import { SubmitCollapse } from '@app/admin/components/SubmitCollapse';
import { callApi } from '@lib/callApi';

/**
* Props accepted by the <VolunteerNotes> component.
*/
export interface VolunteerNotesProps {
/**
* Slug of the event for which a volunteer is being displayed.
*/
event: string;

/**
* Slug of the team that the volunteer is part of.
*/
team: string;

/**
* Information about the volunteer themselves.
*/
volunteer: {
/**
* Unique ID of the volunteer
*/
userId: number;

/**
* Notes associated with the volunteer and their participation.
*/
registrationNotes?: string;
};
}

/**
* The <VolunteerNotes> component displays a form in which the volunteer's notes can be read and
* changed. The same information is available in the scheduling app.
*/
export function VolunteerNotes(props: VolunteerNotesProps) {
const { event, team, volunteer } = props;

const router = useRouter();

const [ error, setError ] = useState<string | undefined>();
const [ invalidated, setInvalidated ] = useState<boolean>(false);
const [ loading, setLoading ] = useState<boolean>(false);

const handleChange = useCallback(() => setInvalidated(true), [ /* no deps */ ]);
const handleSubmit = useCallback(async (data: FieldValues) => {
setLoading(true);
setError(undefined);
try {
const response = await callApi('put', '/api/application/:event/:team/:userId', {
event,
team,
userId: volunteer.userId,

notes: data.notes,
});

if (response.success) {
setInvalidated(false);
router.refresh();
}
} catch (error: any) {
setError(error.message);
} finally {
setLoading(false);
}
}, [ event, router, team, volunteer.userId ]);

return (
<FormContainer defaultValues={{ notes: volunteer.registrationNotes }}
onSuccess={handleSubmit}>
<TextareaAutosizeElement name="notes" fullWidth size="small"
onChange={handleChange} />
<SubmitCollapse error={error} open={invalidated} loading={loading} sx={{ mt: 2 }} />
</FormContainer>
);
}
21 changes: 17 additions & 4 deletions app/admin/events/[slug]/[team]/volunteers/[volunteer]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { notFound } from 'next/navigation';

import EditNoteIcon from '@mui/icons-material/EditNote';
import ScheduleIcon from '@mui/icons-material/Schedule';

import type { NextPageParams } from '@lib/NextRouterParams';
Expand All @@ -23,13 +24,14 @@ import { ApplicationTrainingPreferences } from './ApplicationTrainingPreferences
import { RegistrationStatus } from '@lib/database/Types';
import { VolunteerHeader } from './VolunteerHeader';
import { VolunteerIdentity } from './VolunteerIdentity';
import { VolunteerNotes } from './VolunteerNotes';
import { VolunteerSchedule } from './VolunteerSchedule';
import { getHotelRoomOptions } from '@app/registration/[slug]/application/hotel/getHotelRoomOptions';
import { getTrainingOptions } from '@app/registration/[slug]/application/training/getTrainingOptions';
import { getPublicEventsForFestival, type EventTimeslotEntry } from '@app/registration/[slug]/application/availability/getPublicEventsForFestival';
import { getShiftsForEvent } from '@app/admin/lib/getShiftsForEvent';
import { readSetting } from '@lib/Settings';
import { readUserSetting } from '@lib/UserSettings';
import { readUserSettings } from '@lib/UserSettings';

type RouterParams = NextPageParams<'slug' | 'team' | 'volunteer'>;

Expand Down Expand Up @@ -69,6 +71,7 @@ export default async function EventVolunteerPage(props: RouterParams) {
roleName: tRoles.roleName,
registrationDate: dbInstance.dateTimeAsString(tUsersEvents.registrationDate),
registrationStatus: tUsersEvents.registrationStatus,
registrationNotes: tUsersEvents.registrationNotes,
availabilityEventLimit: tUsersEvents.availabilityEventLimit,
availabilityExceptions: tUsersEvents.availabilityExceptions,
availabilityTimeslots: tUsersEvents.availabilityTimeslots,
Expand Down Expand Up @@ -230,8 +233,13 @@ export default async function EventVolunteerPage(props: RouterParams) {
// ---------------------------------------------------------------------------------------------

const availabilityStep = await readSetting('availability-time-step-minutes');
const defaultExpanded =
await readUserSetting(user.userId, 'user-admin-volunteers-expand-shifts');
const settings = await readUserSettings(user.userId, [
'user-admin-volunteers-expand-notes',
'user-admin-volunteers-expand-shifts',
]);

const notesExpanded = !!settings['user-admin-volunteers-expand-notes'];
const scheduleExpanded = !!settings['user-admin-volunteers-expand-shifts'];

const scheduleSubTitle = `${schedule.length} shift${schedule.length !== 1 ? 's' : ''}`;

Expand All @@ -240,9 +248,14 @@ export default async function EventVolunteerPage(props: RouterParams) {
<VolunteerHeader event={event} team={team} volunteer={volunteer} user={user} />
<VolunteerIdentity event={event.slug} teamId={team.id} userId={volunteer.userId}
contactInfo={contactInfo} volunteer={volunteer} />
<ExpandableSection icon={ <EditNoteIcon color="info" /> } title="Notes"
defaultExpanded={notesExpanded}
setting="user-admin-volunteers-expand-notes">
<VolunteerNotes event={event.slug} team={team.slug} volunteer={volunteer} />
</ExpandableSection>
{ !!schedule.length &&
<ExpandableSection icon={ <ScheduleIcon color="info" /> } title="Schedule"
subtitle={scheduleSubTitle} defaultExpanded={defaultExpanded}
subtitle={scheduleSubTitle} defaultExpanded={scheduleExpanded}
setting="user-admin-volunteers-expand-shifts">
<VolunteerSchedule event={event} schedule={schedule} />
</ExpandableSection> }
Expand Down
45 changes: 42 additions & 3 deletions app/api/application/updateApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ export const kUpdateApplicationDefinition = z.object({
}).optional(),

//------------------------------------------------------------------------------------------
// Update type (3): Application status
// Update type (3): Notes
//------------------------------------------------------------------------------------------

notes: z.string().optional(),

//------------------------------------------------------------------------------------------
// Update type (4): Application status
//------------------------------------------------------------------------------------------

status: z.object({
Expand Down Expand Up @@ -135,6 +141,7 @@ export async function updateApplication(request: Request, props: ActionProps): P
const { eventId, teamId, username } = requestContext;

let affectedRows: number = 0;
let skipLog: boolean = false;

//----------------------------------------------------------------------------------------------
// Update type (1): Application data
Expand Down Expand Up @@ -184,7 +191,39 @@ export async function updateApplication(request: Request, props: ActionProps): P
}

//----------------------------------------------------------------------------------------------
// Update type (3): Application status
// Update type (3): Application notes
//----------------------------------------------------------------------------------------------

if (typeof request.notes === 'string') {
executeAccessCheck(props.authenticationContext, {
check: 'admin-event',
event: request.event,
});

affectedRows = await db.update(tUsersEvents)
.set({
registrationNotes: !!request.notes.length ? request.notes : undefined,
})
.where(tUsersEvents.userId.equals(request.userId))
.and(tUsersEvents.eventId.equals(eventId))
.and(tUsersEvents.teamId.equals(teamId))
.executeUpdate();

skipLog = true;

await Log({
type: LogType.EventVolunteerNotes,
severity: LogSeverity.Info,
sourceUser: props.user,
targetUser: request.userId,
data: {
event: requestContext.event,
},
});
}

//----------------------------------------------------------------------------------------------
// Update type (4): Application status
//----------------------------------------------------------------------------------------------

if (request.status) {
Expand Down Expand Up @@ -260,7 +299,7 @@ export async function updateApplication(request: Request, props: ActionProps): P

// ---------------------------------------------------------------------------------------------

if (!!affectedRows) {
if (!!affectedRows && !skipLog) {
await Log({
type: LogType.AdminUpdateTeamVolunteer,
severity: LogSeverity.Info,
Expand Down
1 change: 1 addition & 0 deletions app/api/auth/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export async function settings(request: Request, props: ActionProps): Promise<Re
'user-admin-shifts-expand-shifts': 'boolean',
'user-admin-volunteers-columns-filter': 'string',
'user-admin-volunteers-columns-hidden': 'string',
'user-admin-volunteers-expand-notes': 'boolean',
'user-admin-volunteers-expand-shifts': 'boolean',
};

Expand Down
5 changes: 5 additions & 0 deletions app/api/event/schedule/PublicSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ export const kPublicSchedule = z.strictObject({
*/
team: z.string(),

/**
* Notes associated with this user, if any. Only shared with leaders.
*/
notes: z.string().optional(),

/**
* Phone number of the volunteer. Only available in certain cases.
*/
Expand Down
4 changes: 4 additions & 0 deletions app/api/event/schedule/getSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,11 @@ async function populateVolunteers(
id: tUsers.userId,
user: {
name: tUsers.name,

avatar: storageJoin.fileHash,
notes: tUsersEvents.registrationNotes,
phoneNumber: tUsers.phoneNumber,

role: {
name: tRoles.roleName,
badge: tRoles.roleBadge,
Expand Down Expand Up @@ -362,6 +365,7 @@ async function populateVolunteers(
role: volunteer.user.role.name,
roleBadge: volunteer.user.role.badge,
team: `${volunteer.user.team.id}`,
notes: isLeader ? volunteer.user.notes : undefined,
phoneNumber: includePhoneNumber ? volunteer.user.phoneNumber : undefined,
// TODO: activeShift
};
Expand Down
1 change: 1 addition & 0 deletions app/lib/Log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum LogType {
DatabaseError = 'database-error',
EventApplication = 'event-application',
EventHelpRequestUpdate = 'event-help-request-update',
EventVolunteerNotes = 'event-volunteer-notes',
ExportDataAccess = 'export-data-access',
}

Expand Down
3 changes: 3 additions & 0 deletions app/lib/LogLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ const kLogMessageFormatter: {
[LogType.EventHelpRequestUpdate]: (source, target, { event, display, mutation }) => {
return `${mutation} a help request from ${display} for ${event}`;
},
[LogType.EventVolunteerNotes]: (source, target, { event }) => {
return `Updated notes for ${target?.name} during ${event}`;
},

[LogType.ExportDataAccess]: (source, target, { event, type }) => {
return `Accessed exported ${event} ${type} data`;
Expand Down
1 change: 1 addition & 0 deletions app/lib/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type UserSettingsMap = {
'user-admin-shifts-expand-shifts': boolean;
'user-admin-volunteers-columns-filter': string;
'user-admin-volunteers-columns-hidden': string;
'user-admin-volunteers-expand-notes': boolean;
'user-admin-volunteers-expand-shifts': boolean;
};

Expand Down
1 change: 1 addition & 0 deletions app/lib/database/scheme/UsersEventsTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class UsersEventsTable extends Table<DBConnection, 'UsersEventsTable'> {
roleId = this.column('role_id', 'int');
registrationDate = this.optionalColumnWithDefaultValue<ZonedDateTime>('registration_date', 'customLocalDateTime', 'dateTime', TemporalTypeAdapter);
registrationStatus = this.columnWithDefaultValue<RegistrationStatus>('registration_status', 'enum', 'RegistrationStatus');
registrationNotes = this.optionalColumnWithDefaultValue('registration_notes', 'string');
shirtFit = this.columnWithDefaultValue<ShirtFit>('shirt_fit', 'enum', 'ShirtFit');
shirtSize = this.optionalColumnWithDefaultValue<ShirtSize>('shirt_size', 'enum', 'ShirtSize');
hotelEligible = this.optionalColumnWithDefaultValue('hotel_eligible', 'int');
Expand Down
20 changes: 16 additions & 4 deletions app/schedule/[event]/volunteers/[volunteer]/VolunteerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import { useCallback, useContext } from 'react';
import { useRouter } from 'next/navigation';

import AlertTitle from '@mui/material/AlertTitle';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import IconButton from '@mui/material/IconButton';
import NotesIcon from '@mui/icons-material/Notes';
import PhoneIcon from '@mui/icons-material/Phone';
import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip';
import WhatsAppIcon from '@mui/icons-material/WhatsApp';

import { Alert } from '../../components/Alert';
import { Avatar } from '@app/components/Avatar';
import { Avatar } from '@components/Avatar';
import { Markdown } from '@components/Markdown';
import { ScheduleContext } from '../../ScheduleContext';
import { SetTitle } from '../../components/SetTitle';
import { callApi } from '@lib/callApi';
Expand Down Expand Up @@ -120,14 +123,14 @@ export function VolunteerPage(props: VolunteerPageProps) {
{ !!phoneNumber &&
<Tooltip title="Give them a call">
<IconButton LinkComponent={Link} href={phoneNumber}>
<PhoneIcon />
<PhoneIcon color="primary" />
</IconButton>
</Tooltip> }
{ !!whatsAppNumber &&
<Tooltip title="Send them a WhatsApp message">
<IconButton LinkComponent={Link} href={whatsAppNumber}
target="_blank">
<WhatsAppIcon />
<WhatsAppIcon color="primary" />
</IconButton>
</Tooltip> }
</Stack>
Expand All @@ -139,8 +142,17 @@ export function VolunteerPage(props: VolunteerPageProps) {
</Avatar>
} />
</Card>
{ /* TODO: Notes */ }
{ !!volunteer.notes &&
<Card sx={{ p: 2 }}>
<Stack direction="row" spacing={2}>
<Box sx={{ height: '1em', minWidth: '40px', textAlign: 'center' }}>
<NotesIcon color="primary" />
</Box>
<Markdown>{volunteer.notes}</Markdown>
</Stack>
</Card> }
{ /* TODO: Schedule */ }
{ /* TODO: Notes editor */ }
</>
);
}
Loading

0 comments on commit 362deb2

Please sign in to comment.