Skip to content

Commit

Permalink
Implement the overview table of the Shifts tool
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Mar 2, 2024
1 parent 0e34c74 commit fdd042a
Show file tree
Hide file tree
Showing 10 changed files with 491 additions and 16 deletions.
31 changes: 31 additions & 0 deletions app/admin/components/CollapsableSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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 Collapse from '@mui/material/Collapse';

import { Section, type SectionProps } from './Section';

/**
* Props accepted by the <CollapsableSection> component, that are directly owned by the component.
*/
export type CollapsableSectionProps = SectionProps & {
/**
* Whether the section should be transitioned in.
*/
in?: boolean;
}

/**
* The <CollapsableSection> component represents a visually separated section of a page in the
* administration area that, unlike <Section>, can be hidden and shown dynamically. The component
* is designed to be compatible with server-side rendering.
*/
export function CollapsableSection(props: React.PropsWithChildren<CollapsableSectionProps>) {
const { in: transitionedIn, ...sectionProps } = props;

return (
<Collapse in={transitionedIn} sx={{ '&.MuiCollapse-hidden': { marginBottom: -2 } }}>
<Section {...sectionProps} />
</Collapse>
);
}
58 changes: 58 additions & 0 deletions app/admin/components/ExcitementIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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 SentimentDissatisfiedIcon from '@mui/icons-material/SentimentDissatisfied';
import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAlt';
import SentimentSatisfiedIcon from '@mui/icons-material/SentimentSatisfied';
import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
import SentimentVerySatisfiedIcon from '@mui/icons-material/SentimentVerySatisfied';
import Tooltip from '@mui/material/Tooltip';

/**
* Props accepted by the <ExcitementIcon> component.
*/
export interface ExcitementIconProps {
/**
* The excitement level, must be between 0 and 1.
*/
excitement: number;
}

/**
* The <ExcitementIcon> component returns an appropriate component visualising the excitement that
* is indicated in the given `props`.
*/
export function ExcitementIcon(props: ExcitementIconProps) {
const { excitement } = props;
if (excitement <= 0.2) {
return (
<Tooltip title="This is a really boring shift">
<SentimentVeryDissatisfiedIcon fontSize="small" color="error" />
</Tooltip>
);
} else if (excitement <= 0.4) {
return (
<Tooltip title="This is a boring shift">
<SentimentDissatisfiedIcon fontSize="small" color="error" />
</Tooltip>
);
} else if (excitement <= 0.6) {
return (
<Tooltip title="This is a dull shift">
<SentimentSatisfiedIcon fontSize="small" color="warning" />
</Tooltip>
);
} else if (excitement <= 0.8) {
return (
<Tooltip title="This is a nice shift">
<SentimentSatisfiedAltIcon fontSize="small" color="success" />
</Tooltip>
);
} else {
return (
<Tooltip title="This is a great shift">
<SentimentVerySatisfiedIcon fontSize="small" color="success" />
</Tooltip>
);
}
}
12 changes: 9 additions & 3 deletions app/admin/components/SectionHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
// 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 type { Privilege } from '@lib/auth/Privileges';
import Typography, { type TypographyProps } from '@mui/material/Typography';

/**
* Props accepted by the <SectionHeader> component.
*/
export interface SectionHeaderProps {
// TODO: Action

/**
* Privilege behind which availability of this section is gated, to inform the volunteer that
* not everyone has access to this information.
*/
privilege?: Privilege;

/**
* Title of this section. Required.
*/
Expand All @@ -17,9 +26,6 @@ export interface SectionHeaderProps {
*/
subtitle?: string;

// TODO: Privilege
// TODO: Action

/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
Expand Down
6 changes: 2 additions & 4 deletions app/admin/events/[slug]/[team]/faq/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { notFound } from 'next/navigation';

import type { NextRouterParams } from '@lib/NextRouterParams';
import { ContentEditor } from '@app/admin/content/ContentEditor';
import { SectionHeader } from '@app/admin/components/SectionHeader';
import { createKnowledgeBaseScope } from '@app/admin/content/ContentScope';
import { generateEventMetadataFn } from '../../../generateEventMetadataFn';
import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo';
Expand All @@ -22,9 +21,8 @@ export default async function EventFaqEntryPage(props: NextRouterParams<'slug' |
const scope = createKnowledgeBaseScope(event.id);

return (
<ContentEditor contentId={parseInt(props.params.id)} pathHidden scope={scope}>
<SectionHeader title="Knowledge base" subtitle={event.shortName} sx={{ mb: 1 }} />
</ContentEditor>
<ContentEditor contentId={parseInt(props.params.id)} pathHidden scope={scope}
title="Knowledge base" subtitle={event.shortName} />
);
}

Expand Down
140 changes: 140 additions & 0 deletions app/admin/events/[slug]/[team]/shifts/ShiftTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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 Link from 'next/link';

import { default as MuiLink } from '@mui/material/Link';
import NewReleasesIcon from '@mui/icons-material/NewReleases';
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';

/**
* Formats the given number of `minutes` to a HH:MM string.
*/
function formatMinutes(minutes: number): string {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes - hours * 60;

return remainingMinutes ? `${hours}:${('00' + remainingMinutes).substr(-2)}`
: `${hours}`;
}

/**
* Props accepted by the <ShiftTable> component.
*/
export type ShiftTableProps = EventShiftContext['context'] & {
/**
* Whether the shift table should be shown in read only mode.
*/
readOnly?: boolean;
};

/**
* The <ShiftTable> component is a Data Table that allows rows to be shown and deleted, which
* displays the shifts that exist for a particular { event, team } pair.
*/
export function ShiftTable(props: ShiftTableProps) {
const { readOnly, ...context } = props;

const deleteColumn: RemoteDataTableColumn<EventShiftRowModel>[] = [];
if (!readOnly) {
deleteColumn.push({
field: 'id',
headerName: /* empty= */ '',
sortable: false,
width: 50,
});
}

const columns: RemoteDataTableColumn<EventShiftRowModel>[] = [
...deleteColumn,
{
field: 'name',
headerName: 'Shift',
flex: 1,

renderCell: params =>
<MuiLink component={Link} href={`./shifts/${params.row.id}`}>
{params.value}
</MuiLink>,
},
{
field: 'hours',
headerName: 'Scheduled',
flex: 1,

renderCell: params => {
if (!params.value) {
return (
<Typography variant="body2" sx={{ color: 'text.disabled' }}>
</Typography>
);
}

return <>{formatMinutes(params.value)} hours</>;
},
},
{
field: 'activityId',
headerAlign: 'center',
headerName: /* empty= */ '',
sortable: false,
align: 'center',
width: 50,

renderHeader: params =>
<Tooltip title="Initiative or program?">
<NewReleasesIcon fontSize="small" color="primary" />
</Tooltip>,

renderCell: params => {
if (!params.value) {
return (
<Tooltip title="(our initiative)">
<NewReleasesIcon fontSize="small" color="disabled" />
</Tooltip>
);
}

const href = `../program/activities/${params.row.activityId}`;
return (
<Tooltip title={params.row.activityName}>
<MuiLink component={Link} href={href} sx={{ pt: '5px' }}>
<NewReleasesIcon fontSize="small" color="success" />
</MuiLink>
</Tooltip>
);
}
},
{
field: 'excitement',
headerAlign: 'center',
headerName: /* empty= */ '',
sortable: false,
align: 'center',
width: 50,

renderHeader: params =>
<Tooltip title="Volunteer sentiment">
<SentimentSatisfiedAltIcon fontSize="small" color="primary" />
</Tooltip>,

renderCell: params =>
<ExcitementIcon excitement={params.value} />,
}
];

return (
<RemoteDataTable columns={columns} endpoint="/api/admin/event/shifts" context={context}
defaultSort={{ field: 'id', sort: 'asc' }} subject="shift"
enableDelete={!readOnly} pageSize={100} disableFooter />
);
}
29 changes: 23 additions & 6 deletions app/admin/events/[slug]/[team]/shifts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

import type { NextRouterParams } from '@lib/NextRouterParams';
import { CollapsableSection } from '@app/admin/components/CollapsableSection';
import { Privilege, can } from '@lib/auth/Privileges';
import { Section } from '@app/admin/components/Section';
import { SectionIntroduction } from '@app/admin/components/SectionIntroduction';
import { ShiftTable } from './ShiftTable';
import { generateEventMetadataFn } from '../../generateEventMetadataFn';
import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo';

Expand All @@ -13,17 +16,31 @@ import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndF
* end up out of sync.
*/
export default async function EventTeamShiftsPage(props: NextRouterParams<'slug' | 'team'>) {
const { event, team } = await verifyAccessAndFetchPageInfo(props.params);
const { event, team, user } = await verifyAccessAndFetchPageInfo(props.params);

// TODO: Mutable list with all the shifts that exist during the convention.
// TODO: Box with warnings regarding the shifts (e.g. out-of-sync entries).

const readOnly = !can(user, Privilege.EventShiftManagement);
const warnings: any[] = [ ];

return (
<Section title="Shifts" subtitle={team.name}>
<SectionIntroduction important>
The shifts tool has not been implemented yet.
</SectionIntroduction>
</Section>
<>
<Section title="Shifts" subtitle={team.name}>
<ShiftTable event={event.slug} team={team.slug} readOnly={readOnly} />
</Section>
<CollapsableSection in={!!warnings.length} title="Shift warnings">
<SectionIntroduction important>
The shifts tool has not been implemented yet.
</SectionIntroduction>
</CollapsableSection>
{ !readOnly &&
<Section title="Create a new shift" privilege={Privilege.EventShiftManagement}>
<SectionIntroduction important>
The shifts tool has not been implemented yet.
</SectionIntroduction>
</Section> }
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ export function RequestDataTable(props: RequestDataTableProps) {
return params.value;

return (
<Typography variant="body2"
sx={{ color: 'text.disabled', fontStyle: 'italic' }}>
<Typography variant="body2" sx={{ color: 'text.disabled' }}>
</Typography>
);
Expand Down
Loading

0 comments on commit fdd042a

Please sign in to comment.