Skip to content

Commit

Permalink
Introduce colours and categories on the Shifts page
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Mar 3, 2024
1 parent f402192 commit c2a0958
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 19 deletions.
39 changes: 39 additions & 0 deletions app/admin/components/Square.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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.

import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';

/**
* Props accepted by the <Square> component.
*/
export interface SquareProps {
/**
* Colour, in an HTML-renderable format, to render the square in.
*/
colour: string;

/**
* Tooltip title to show when hovering over the square.
*/
title: string;
}

/**
* The <Square> component draws a rectangular square with a particular colour, featuring a tooltip
* that explains what the colour is indicating.
*/
export function Square(props: SquareProps) {
return (
<Tooltip title={props.title}>
<Box sx={{
backgroundColor: props.colour,
border: '1px solid transparent',
borderColor: 'divider',
borderRadius: 1,
height: '21px',
width: '21px',
}} />
</Tooltip>
);
}
27 changes: 25 additions & 2 deletions app/admin/events/[slug]/[team]/shifts/ShiftTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import Link from 'next/link';

import { default as MuiLink } from '@mui/material/Link';
import NewReleasesIcon from '@mui/icons-material/NewReleases';
import PaletteIcon from '@mui/icons-material/Palette';
import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAlt';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';

import type { EventShiftContext, EventShiftRowModel } from '@app/api/admin/event/shifts/[[...id]]/route';
import { ExcitementIcon } from '@app/admin/components/ExcitementIcon';
import { RemoteDataTable, type RemoteDataTableColumn } from '@app/admin/components/RemoteDataTable';
import { Square } from '@app/admin/components/Square';

/**
* Formats the given number of `minutes` to a HH:MM string.
Expand Down Expand Up @@ -47,18 +49,39 @@ export function ShiftTable(props: ShiftTableProps) {
if (!readOnly) {
deleteColumn.push({
field: 'id',
headerName: /* empty= */ '',
headerName: /* no header= */ '',
sortable: false,
width: 50,
});
}

const columns: RemoteDataTableColumn<EventShiftRowModel>[] = [
...deleteColumn,
{
field: 'colour',
headerAlign: 'center',
headerName: /* no header= */ '',
sortable: false,
align: 'center',
width: 50,

renderHeader: params =>
<Tooltip title="Colour assigned to this shift">
<PaletteIcon fontSize="small" color="primary" />
</Tooltip>,

renderCell: params =>
<Square colour={params.value} title="Colour assigned to this shift" />,
},
{
field: 'category',
headerName: 'Category',
width: 200,
},
{
field: 'name',
headerName: 'Shift',
flex: 1,
flex: 2,

renderCell: params =>
<MuiLink component={Link} href={`./shifts/${params.row.id}`}>
Expand Down
19 changes: 4 additions & 15 deletions app/admin/events/[slug]/program/requests/RequestDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';

import type { ProgramRequestContext, ProgramRequestRowModel } from '@app/api/admin/program/requests/[[...id]]/route';
import { type RemoteDataTableColumn, RemoteDataTable } from '@app/admin/components/RemoteDataTable';
import { RemoteDataTable, type RemoteDataTableColumn } from '@app/admin/components/RemoteDataTable';
import { Square } from '@app/admin/components/Square';

/**
* Information about a particular team shown on the request overview table.
Expand Down Expand Up @@ -169,20 +170,8 @@ export function RequestDataTable(props: RequestDataTableProps) {
align: 'center',
width: 50,

renderHeader: params => {
return (
<Tooltip title={team.name}>
<Box sx={{
backgroundColor: team.colour,
border: '1px solid transparent',
borderColor: 'divider',
borderRadius: 1,
height: '21px',
width: '21px',
}} />
</Tooltip>
);
},
renderHeader: params =>
<Square colour={team.colour} title={team.name} />,

renderCell: params => {
if (!Object.hasOwn(params.row.shifts, team.id)) {
Expand Down
4 changes: 4 additions & 0 deletions app/admin/volunteers/shifts/ShiftCategoriesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export function ShiftCategoriesTable() {
editable: false,
sortable: false,
width: 50,

// The default shift category cannot be removed, as it's hardcoded in the source when
// new shifts are being created. It can be freely updated, of course.
isProtected: params => params.value === /* default= */ 1,
},
{
field: 'name',
Expand Down
49 changes: 47 additions & 2 deletions app/api/admin/event/shifts/[[...id]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { z } from 'zod';
import { type DataTableEndpoints, createDataTableApi } from '../../../../createDataTableApi';
import { Log, LogSeverity, LogType } from '@lib/Log';
import { Privilege } from '@lib/auth/Privileges';
import { createColourInterpolator, type ColourInterpolator } from '@lib/ColourInterpolator';
import { executeAccessCheck } from '@lib/auth/AuthenticationContext';
import db, { tActivities, tEventsTeams, tEvents, tTeams, tSchedule, tShifts } from '@lib/database';
import db, { tActivities, tEventsTeams, tEvents, tTeams, tSchedule, tShifts, tShiftsCategories }
from '@lib/database';

/**
* Row model for a team's shifts. The shifts are fully mutable, even though the create and edit
Expand All @@ -25,6 +27,16 @@ const kEventShiftRowModel = z.object({
*/
name: z.string().optional(),

/**
* The colour that's assigned to this shift, calculated by the server.
*/
colour: z.string(),

/**
* Name of the category that this shift is part of.
*/
category: z.string(),

/**
* Number of hours that volunteers have been scheduled to work on this shift.
*/
Expand Down Expand Up @@ -163,6 +175,9 @@ export const { GET, PUT } = createDataTableApi(kEventShiftRowModel, kEventShiftC
if (!event || !team)
notFound();

if (sort?.field === 'colour')
throw new Error(`Invalid sorting key provided (v=${sort.field})`);

const dbInstance = db;

const activitiesJoin = tActivities.forUseInLeftJoin();
Expand All @@ -173,6 +188,8 @@ export const { GET, PUT } = createDataTableApi(kEventShiftRowModel, kEventShiftC
${scheduleJoin.scheduleTimeEnd})`;

const result = await dbInstance.selectFrom(tShifts)
.innerJoin(tShiftsCategories)
.on(tShiftsCategories.shiftCategoryId.equals(tShifts.shiftCategoryId))
.leftJoin(activitiesJoin)
.on(activitiesJoin.activityId.equals(tShifts.shiftActivityId))
.and(activitiesJoin.activityFestivalId.equalsIfValue(event.festivalId))
Expand All @@ -184,6 +201,8 @@ export const { GET, PUT } = createDataTableApi(kEventShiftRowModel, kEventShiftC
.select({
id: tShifts.shiftId,
name: tShifts.shiftName,
category: tShiftsCategories.shiftCategoryName,
categoryColour: tShiftsCategories.shiftCategoryColour,
hours: dbInstance.sum(shiftDurationFragment),
activityId: activitiesJoin.activityId,
activityName: activitiesJoin.activityTitle,
Expand All @@ -193,10 +212,36 @@ export const { GET, PUT } = createDataTableApi(kEventShiftRowModel, kEventShiftC
.orderBy(sort?.field ?? 'name', sort?.sort ?? 'asc')
.executeSelectMany();

const categories = new Map</* name= */ string, ColourInterpolator>;
const categoryCounts = new Map</* name= */ string, [ number, number ]>;

for (const { id, category, categoryColour } of result) {
if (!categories.has(category))
categories.set(category, createColourInterpolator(categoryColour));

const currentCounts = categoryCounts.get(category);
if (!currentCounts)
categoryCounts.set(category, [ 0, 0 ]);
else
categoryCounts.set(category, [ currentCounts[0] + 1, currentCounts[1] + 1 ]);
}

return {
success: true,
rowCount: result.length,
rows: result,
rows: result.map(shift => {
const [ shiftsInCategory, remaining ] = categoryCounts.get(shift.category)!;

const colourInterpolator = categories.get(shift.category)!;
const colour = colourInterpolator(remaining / shiftsInCategory);

categoryCounts.set(shift.category, [ shiftsInCategory, remaining - 1 ]);

return {
...shift,
colour,
};
}),
};
},

Expand Down
1 change: 1 addition & 0 deletions app/lib/database/scheme/ShiftsTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class ShiftsTable extends Table<DBConnection, 'ShiftsTable'> {
shiftIdentifier = this.column('shift_identifier', 'string');
eventId = this.column('event_id', 'int');
teamId = this.column('team_id', 'int');
shiftCategoryId = this.column('shift_category_id', 'int');
shiftName = this.column('shift_name', 'string');
shiftActivityId = this.optionalColumnWithDefaultValue('shift_activity_id', 'int');
shiftAreaId = this.optionalColumnWithDefaultValue('shift_area_id', 'int');
Expand Down
6 changes: 6 additions & 0 deletions ts-sql.scheme.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,11 @@ tables:
nullable: false
default: null
comment: ""
- name: shift_category_id
type: int(8) unsigned
nullable: false
default: null
comment: ""
- name: shift_name
type: varchar(64)
nullable: false
Expand Down Expand Up @@ -717,6 +722,7 @@ tables:
`shift_identifier` varchar(8) NOT NULL,
`event_id` int(4) unsigned NOT NULL,
`team_id` int(4) unsigned NOT NULL,
`shift_category_id` int(8) unsigned NOT NULL,
`shift_name` varchar(64) NOT NULL,
`shift_activity_id` int(4) unsigned DEFAULT NULL,
`shift_area_id` int(4) unsigned DEFAULT NULL,
Expand Down

0 comments on commit c2a0958

Please sign in to comment.