Skip to content

Commit

Permalink
Add detail pages to SMS and WhatsApp outbox views
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Apr 17, 2024
1 parent 56a653e commit a01d130
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 8 deletions.
2 changes: 1 addition & 1 deletion app/admin/system/outbox/OutboxNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function OutboxNavigation() {
]), [ /* no dependencies */ ]);

const [ selectedTabIndex, setSelectedTabIndex ] =
useState<number | undefined>(/* Requests= */ 3);
useState<number | undefined>(/* E-mail= */ 0);

useEffect(() => {
for (let index = 0; index < navigationOptions.length; ++index) {
Expand Down
2 changes: 1 addition & 1 deletion app/admin/system/outbox/TwilioDataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved.
// 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';
Expand Down
255 changes: 255 additions & 0 deletions app/admin/system/outbox/TwilioDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// 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 Link from 'next/link';
import { notFound } from 'next/navigation';

import { default as MuiLink } from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';

import { Temporal, formatDate } from '@lib/Temporal';
import { TwilioOutboxType } from '@lib/database/Types';
import db, { tOutboxTwilio, tUsers } from '@lib/database';
import { WebhookDataTable } from '../webhooks/WebhookDataTable';

/**
* Props accepted by the <TwilioDetailsPage> component.
*/
export interface TwilioDetailsPageProps {
/**
* Type of message details should be shown for.
*/
type: TwilioOutboxType;

/**
* Unique ID of the message that should be detailed.
*/
id: number;
}

/**
* The <TwilioDetailsPage> component displays a detail page listing all information known about a
* particular Twilio message in the outbox.
*/
export async function TwilioDetailsPage(props: TwilioDetailsPageProps) {
const recipientUserJoin = tUsers.forUseInLeftJoinAs('ruj');
const senderUserJoin = tUsers.forUseInLeftJoinAs('suj');

const dbInstance = db;
const message = await dbInstance.selectFrom(tOutboxTwilio)
.leftJoin(senderUserJoin)
.on(senderUserJoin.userId.equals(tOutboxTwilio.outboxSenderUserId))
.leftJoin(recipientUserJoin)
.on(recipientUserJoin.userId.equals(tOutboxTwilio.outboxRecipientUserId))
.where(tOutboxTwilio.outboxType.equals(props.type))
.and(tOutboxTwilio.outboxTwilioId.equals(props.id))
.select({
date: tOutboxTwilio.outboxTimestamp,
sender: {
name: tOutboxTwilio.outboxSender,
user: {
id: senderUserJoin.userId,
name: senderUserJoin.name,
},
},
recipient: {
name: tOutboxTwilio.outboxRecipient,
user: {
id: recipientUserJoin.userId,
name: recipientUserJoin.name,
},
},
message: tOutboxTwilio.outboxMessage,
error: {
code: tOutboxTwilio.outboxResultErrorCode,
message: tOutboxTwilio.outboxResultErrorMessage,
},
exception: {
name: tOutboxTwilio.outboxErrorName,
cause: tOutboxTwilio.outboxErrorCause,
message: tOutboxTwilio.outboxErrorMessage,
stack: tOutboxTwilio.outboxErrorStack,
},
result: {
status: tOutboxTwilio.outboxResultStatus,
sid: tOutboxTwilio.outboxResultSid,
time: tOutboxTwilio.outboxResultTime,
},
})
.executeSelectNoneOrOne();

if (!message)
notFound();

return (
<Stack direction="column" spacing={2}>
<Typography variant="h5">
Message sent on {
formatDate(
Temporal.ZonedDateTime.from(message.date).withTimeZone(
Temporal.Now.timeZoneId()),
'MMMM D, YYYY [at] H:mm:ss') }
</Typography>
<TableContainer component={Paper} variant="outlined">
<Table>
<TableBody>
<TableRow>
<TableCell width="25%" component="th" scope="row">
From
</TableCell>
<TableCell>
{ (!message.sender || !message.sender.name) &&
<Typography sx={{ color: 'text.disabled', fontStyle: 'italic' }}
component="span" variant="body2">
Unknown
</Typography> }

{ (!!message.sender && !!message.sender.name) &&
<Typography component="span" variant="body2">
{message.sender.name}
</Typography> }

{ (!!message.sender && !!message.sender.user?.name) && ' — ' }
{ (!!message.sender && !!message.sender.user?.name) &&
<MuiLink component={Link}
href={`/admin/volunteers/${message.sender.user.id}`}>
{message.sender.user.name}
</MuiLink> }
</TableCell>
</TableRow>
<TableRow>
<TableCell width="25%" component="th" scope="row">
To
</TableCell>
<TableCell>
{message.recipient.name}

{ !!message.recipient.user && ' — ' }
{ !!message.recipient.user &&
<MuiLink component={Link}
href={`/admin/volunteers/${message.recipient.user.id}`}>
{message.recipient.user.name}
</MuiLink> }
</TableCell>
</TableRow>
{ props.type === TwilioOutboxType.WhatsApp &&
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="warning" variant="standard">
WhatsApp messages are based on templates, so the message may
not be immediately useful.
</Alert>
</TableCell>
</TableRow> }
<TableRow>
<TableCell width="25%" component="th" scope="row">
Message
</TableCell>
<TableCell sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>
{message.message}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
{ !!message.error &&
<TableContainer component={Paper} variant="outlined">
<Table>
<TableBody>
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="error">
An error occurred when sending this message.
</Alert>
</TableCell>
</TableRow>
<TableRow>
<TableCell width="25%" component="th" scope="row">
Error code
</TableCell>
<TableCell>{message.error.code}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Error message
</TableCell>
<TableCell>{message.error.message}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer> }
{ !!message.exception &&
<TableContainer component={Paper} variant="outlined">
<Table>
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="error">
An exception occurred when sending this message.
</Alert>
</TableCell>
</TableRow>
<TableRow>
<TableCell width="25%" component="th" scope="row">Error name</TableCell>
<TableCell>{message.exception.name}</TableCell>
</TableRow>
{ !!message.exception.message &&
<TableRow>
<TableCell component="th" scope="row">Error message</TableCell>
<TableCell>{message.exception.message}</TableCell>
</TableRow> }
{ !!message.exception.stack &&
<TableRow>
<TableCell component="th" scope="row">Stack trace</TableCell>
<TableCell sx={{whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'}}>
{message.exception.stack}
</TableCell>
</TableRow> }
{ !!message.exception.cause &&
<TableRow>
<TableCell component="th" scope="row">Cause</TableCell>
<TableCell sx={{whiteSpace: 'pre-wrap', overflowWrap: 'anywhere'}}>
{JSON.parse(message.exception.cause)}
</TableCell>
</TableRow> }
</Table>
</TableContainer> }
{ !!message.result &&
<TableContainer component={Paper} variant="outlined">
<Table>
<TableBody>
{ !!message.result.status &&
<TableRow>
<TableCell width="25%" component="th" scope="row">
Status
</TableCell>
<TableCell>{message.result.status}</TableCell>
</TableRow> }
{ !!message.result.sid &&
<TableRow>
<TableCell width="25%" component="th" scope="row">
SID
</TableCell>
<TableCell>{message.result.sid}</TableCell>
</TableRow> }
<TableRow>
<TableCell width="25%" component="th" scope="row">
Time
</TableCell>
<TableCell>{message.result.time}ms</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer> }
{ !!message.result && !!message.result.sid &&
<WebhookDataTable twilioMessageSid={message.result.sid} /> }
</Stack>
);
}
8 changes: 8 additions & 0 deletions app/admin/system/outbox/email/[id]/EmailMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';

import { default as MuiLink } from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import Paper from '@mui/material/Paper';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
Expand Down Expand Up @@ -132,6 +133,13 @@ export function EmailMessage(props: EmailMessageProps) {
{ !!message.errorName &&
<TableContainer component={Paper} variant="outlined">
<Table>
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="error">
An exception occurred when sending this message.
</Alert>
</TableCell>
</TableRow>
<TableRow>
<TableCell width="25%" component="th" scope="row">Error name</TableCell>
<TableCell>{message.errorName}</TableCell>
Expand Down
27 changes: 27 additions & 0 deletions app/admin/system/outbox/sms/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 { Metadata } from 'next';

import type { NextPageParams } from '@lib/NextRouterParams';
import { Privilege } from '@lib/auth/Privileges';
import { TwilioDetailsPage } from '../../TwilioDetailsPage';
import { TwilioOutboxType } from '@lib/database/Types';
import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext';

/**
* The outbox page details an outgoing SMS message, with all information we have collected in
* the database regarding delivery of that message.
*/
export default async function OutboxSmsDetailsPage({ params }: NextPageParams<'id'>) {
await requireAuthenticationContext({
check: 'admin',
privilege: Privilege.SystemOutboxAccess,
});

return <TwilioDetailsPage type={TwilioOutboxType.SMS} id={parseInt(params.id, 10)} />;
}

export const metadata: Metadata = {
title: 'SMS | Outbox | AnimeCon Volunteer Manager',
};
27 changes: 27 additions & 0 deletions app/admin/system/outbox/whatsapp/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 { Metadata } from 'next';

import type { NextPageParams } from '@lib/NextRouterParams';
import { Privilege } from '@lib/auth/Privileges';
import { TwilioDetailsPage } from '../../TwilioDetailsPage';
import { TwilioOutboxType } from '@lib/database/Types';
import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext';

/**
* The outbox page details an outgoing WhatsApp message, with all information we have collected in
* the database regarding delivery of that message.
*/
export default async function OutboxWhatsAppDetailsPage({ params }: NextPageParams<'id'>) {
await requireAuthenticationContext({
check: 'admin',
privilege: Privilege.SystemOutboxAccess,
});

return <TwilioDetailsPage type={TwilioOutboxType.WhatsApp} id={parseInt(params.id, 10)} />;
}

export const metadata: Metadata = {
title: 'WhatsApp | Outbox | AnimeCon Volunteer Manager',
};
15 changes: 13 additions & 2 deletions app/admin/system/webhooks/WebhookDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,21 @@ const kServiceColours = {
*/
const kMessageSizeUnit = [ 'bytes', 'KiB', 'MiB', 'GiB' ];

/**
* Props accepted by the <WebhookDataTable> component.
*/
export interface WebhookDataTableProps {
/**
* Filter for Twilio webhooks to filter by a particular message SID.
*/
twilioMessageSid?: string;
}

/**
* The <WebhookDataTable> component displays all webhook calls received by the Volunteer Manager.
* Each links through to a detailed page with all information regarding that particular webhook.
*/
export function WebhookDataTable() {
export function WebhookDataTable(props: WebhookDataTableProps) {
const localTz = Temporal.Now.timeZoneId();
const columns: RemoteDataTableColumn<WebhookRowModel>[] = [
{
Expand All @@ -45,7 +55,7 @@ export function WebhookDataTable() {
width: 50,

renderCell: params => {
const href = `./webhooks/${params.row.service}/${params.id}`;
const href = `/admin/system/webhooks/${params.row.service}/${params.id}`;
return (
<MuiLink component={Link} href={href} sx={{ pt: '5px' }}>
<ReadMoreIcon color="info" />
Expand Down Expand Up @@ -160,5 +170,6 @@ export function WebhookDataTable() {
];

return <RemoteDataTable columns={columns} endpoint="/api/admin/webhooks"
context={{ foo: 'bar', ...props }}
defaultSort={{ field: 'id', sort: 'desc' }} pageSize={50} />;
}
Loading

0 comments on commit a01d130

Please sign in to comment.