Files
noahspan-flying/app/src/components/logbook/Logbook.tsx
2025-03-23 10:23:30 -05:00

310 lines
9.1 KiB
TypeScript

import { useEffect, useReducer } from 'react';
import LogForm from '../logForm/LogForm';
import {
Alert,
Box,
Button,
ColumnDef,
Grid,
Icon,
IconButton,
IconName,
Spinner,
Table,
theme,
Typography,
useMediaQuery
} from '@noahspan/noahspan-components';
import { initialState, reducer } from './reducer';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
import { authColumns, unauthColumns } from './columns';
import ActionMenu from '../actionMenu/ActionMenu';
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
import { ILogbookEntry } from './ILogbookEntry';
import LogbookCard from '../logbookCard/LogbookCard';
import LogTracks from '../logTracks/LogTracks';
const Logbook: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
const actionsColumn: ColumnDef<ILogbookEntry> = {
header: 'Actions',
meta: {
align: 'center',
headerAlign: 'center'
},
cell: (info: any) => (
<ActionMenu
id={info.row.original.rowKey}
onDelete={onDeleteEntry}
onOpenCloseForm={onOpenCloseEntryForm}
onOpenCloseTracks={onOpenCloseTracks}
/>
)
}
const tracksColumn: ColumnDef<ILogbookEntry> = {
accessorKey: 'tracks',
header: 'Tracks',
cell: (info: any) => {
if (info.row.original.tracks && info.row.original.tracks.length > 0) {
return (
<IconButton onClick={() => onOpenCloseTracks(FormMode.VIEW, info.row.original.rowKey)}><Icon iconName={IconName.MAP_LOCATION_DOT} /></IconButton>
)
}
}
}
const getLogbookEntries = async () => {
try {
dispatch({ type: 'SET_IS_LOADING', payload: true });
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(`api/logs`, config);
const entries: ILogbookEntry[] = response.data;
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
if (response.data.length > 0) {
dispatch({ type: 'SET_ENTRIES', payload: response.data });
if (state.alert) {
dispatch({ type: 'SET_ALERT', payload: undefined})
}
} else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No logbook entries found.'}})
}
} catch (error) {
const axiosError = error as AxiosError;
dispatch({
type: 'SET_ALERT',
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
});
} finally {
dispatch({ type: 'SET_IS_LOADING', payload: false });
}
};
const onOpenCloseEntryForm = (mode: FormMode, entryId?: string) => {
switch (mode) {
case FormMode.ADD:
case FormMode.EDIT:
case FormMode.VIEW:
dispatch({
type: 'SET_OPEN_CLOSE_ENTRY_FORM',
payload: {
formMode: mode,
selectedEntryId: entryId,
isFormOpen: true
}
});
break;
case FormMode.CANCEL:
dispatch({
type: 'SET_OPEN_CLOSE_ENTRY_FORM',
payload: {
formMode: mode,
selectedEntryId: undefined,
isFormOpen: false
}
});
break;
}
};
const onOpenCloseTracks = (mode: FormMode, rowKey?: string) => {
switch(mode) {
case FormMode.EDIT:
case FormMode.VIEW:
dispatch({
type: 'SET_OPEN_CLOSE_TRACKS',
payload: {
tracksMode: mode,
isTracksOpen: true,
selectedRowKey: rowKey
}
})
break;
case FormMode.CANCEL:
dispatch({
type: 'SET_OPEN_CLOSE_TRACKS',
payload: {
tracksMode: mode,
isTracksOpen: false,
selectedRowKey: undefined
}
})
break;
}
}
const onDeleteEntry = (entryId: string) => {
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: true, selectedEntryId: entryId }
});
};
const onConfirmationDialogConfirm = async () => {
try {
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
const token = await getAccessToken();
const config = isAuthenticated
? { headers: { Authorization: `${token}` } }
: {};
await httpClient.delete(`api/logs/log/${state.selectedEntryId}`, config);
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: false, selectedEntryId: undefined }
});
await getLogbookEntries();
} catch (error) {
const axiosError = error as AxiosError;
dispatch({
type: 'SET_ALERT',
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
});
} finally {
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
}
};
const onConfirmationDialogCancel = () => {
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: false, selectedEntryId: undefined }
});
};
useEffect(() => {
let newColumns: ColumnDef<ILogbookEntry>[];
if (isAuthenticated) {
newColumns = [...authColumns];
} else {
newColumns = [...unauthColumns];
}
const actionsColumnExists = newColumns.find((column) => column.id === 'actions');
const tracksColumnExists = newColumns.find((column) => column.id === 'actions');
if (!actionsColumnExists) {
newColumns.push(actionsColumn);
}
if (!tracksColumnExists) {
const notesColumnIndex = newColumns.findIndex((column) => column.id === 'notes')
newColumns.splice(notesColumnIndex, 0, tracksColumn)
}
dispatch({ type: 'SET_COLUMNS', payload: newColumns })
}, [isAuthenticated])
useEffect(() => {
if (!state.isFormOpen) {
getLogbookEntries();
}
}, [state.isFormOpen, state.isTracksOpen]);
return (
<Box sx={{ margin: '20px' }}>
<Grid container spacing={2}>
<Grid size={isMedium ? 11 : 6}>
<Typography variant="h4">Logbook</Typography>
</Grid>
<Grid display="flex" justifyContent="right" size={isMedium ? 1 : 6}>
{isAuthenticated &&
<Button
onClick={() => onOpenCloseEntryForm(FormMode.ADD)}
startIcon={<Icon iconName={IconName.PLUS} />}
variant="contained"
data-testid="pilot-add-button"
>
Add Entry
</Button>
}
</Grid>
{!state.isLoading && state.alert && (
<Grid display="flex" justifyContent="center" size={12}>
<Alert
onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined })
}
severity={state.alert.severity}
sx={{ width: '100%' }}
>
{state.alert.message}
</Alert>
</Grid>
)}
{!state.isLoading && (
<Grid size={12}>
{isMedium && state.columns && state.columns.length > 0 && state.entries.length > 0 && (
<Table columns={state.columns} data={state.entries} />
)}
{!isMedium && state.entries.length > 0 &&
<LogbookCard logs={state.entries} onDelete={onDeleteEntry} mode='logbook' onOpenCloseForm={onOpenCloseEntryForm} />
}
</Grid>
)}
{state.isLoading && !state.alert && (
<>
<Grid display="flex" justifyContent="center" size={12}>
<Spinner />
</Grid>
<Grid display="flex" justifyContent="center" size={12}>
Loading...
</Grid>
</>
)}
</Grid>
{state.isFormOpen && (
<LogForm
entryId={state.selectedEntryId}
isDrawerOpen={state.isFormOpen}
mode={state.formMode}
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
/>
)}
{state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete the logbook entry?"
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmationDialogCancel}
onConfirm={onConfirmationDialogConfirm}
title="Confirm Delete"
/>
)}
{state.isTracksOpen &&
<LogTracks
isDrawerOpen={state.isTracksOpen}
mode={state.tracksMode}
onOpenClose={(mode) => onOpenCloseTracks(mode)}
selectedRowKey={state.selectedEntryId}
/>
}
</Box>
);
};
export default Logbook;