adding logbook entry delete

This commit is contained in:
2024-11-19 20:31:58 -06:00
parent d8180bb7ee
commit 30701d3ada
14 changed files with 437 additions and 103 deletions

View File

@@ -1,6 +1,7 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
@@ -79,4 +80,17 @@ export class LogbookController {
});
}
}
@Delete(':rowKey')
async delete(@Param() params: any): Promise<void> {
try {
await this.logbookService.delete(params.rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
}

View File

@@ -10,19 +10,19 @@ export class LogbookEntity {
routeTo: string;
durationOfFlight: number | null;
singleEngineLand: number | null;
simulatorAtd: number | null;
landingsDay: number | null;
landingsNight: number | null;
groundTrainingReceived: number;
flightTrainingReceived: number;
crossCountry: number | null;
night: number | null;
solo: number | null;
pilotInCommand: number | null;
instrumentActual: number | null;
instrumentSimulated: number | null;
instrumentApproaches: number | null;
instrumentHolds: number | null;
instrumentNavTrack: number | null;
notes: string;
simulatorAtd?: number | null;
landingsDay?: number | null;
landingsNight?: number | null;
groundTrainingReceived?: number;
flightTrainingReceived?: number;
crossCountry?: number | null;
night?: number | null;
solo?: number | null;
pilotInCommand?: number | null;
instrumentActual?: number | null;
instrumentSimulated?: number | null;
instrumentApproaches?: number | null;
instrumentHolds?: number | null;
instrumentNavTrack?: number | null;
notes?: string;
}

View File

@@ -73,7 +73,7 @@ export class LogbookService {
instrumentNavTrack: entity.instrumentNavTrack
? Number(entity.instrumentNavTrack)
: null,
notes: entity.notes.toString()
notes: entity.notes ? entity.notes.toString() : ''
};
logbookEntries.push(logbookEntry);
@@ -143,14 +143,14 @@ export class LogbookService {
}
async update(logbookData: LogbookDto): Promise<void> {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
const logbook: LogbookEntity = new LogbookEntity();
Object.assign(logbook, logbookData);
try {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
const logbook: LogbookEntity = new LogbookEntity();
Object.assign(logbook, logbookData);
await client.upsertEntity(logbook, 'Replace');
} catch (error) {
const restError: RestError = error as RestError;
@@ -162,4 +162,22 @@ export class LogbookService {
);
}
}
async delete(rowKey: string): Promise<void> {
try {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
await client.deleteEntity('entry', rowKey);
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
}

View File

@@ -11,9 +11,9 @@ import {
PenIcon,
TrashIcon
} from '@noahspan/noahspan-components';
import { FormMode } from '../enums/formMode';
import { FormMode } from '../../enums/formMode';
const ActionMenu = ({ id, onOpenCloseForm }: IActionMenuProps) => {
const ActionMenu = ({ id, onDelete, onOpenCloseForm }: IActionMenuProps) => {
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
null
);
@@ -50,7 +50,7 @@ const ActionMenu = ({ id, onOpenCloseForm }: IActionMenuProps) => {
<ListItemText>View</ListItemText>
</MenuItem>
<hr className="my-3" />
<MenuItem>
<MenuItem onClick={() => onDelete(id)}>
<ListItemIcon>
<TrashIcon size="lg" />
</ListItemIcon>

View File

@@ -1,6 +1,7 @@
import { FormMode } from '../enums/formMode';
import { FormMode } from '../../enums/formMode';
export interface IActionMenuProps {
id: string;
onDelete: (entryId: string) => void;
onOpenCloseForm: (formMode: FormMode, id: string) => void;
}

View File

@@ -0,0 +1,52 @@
import {
Box,
Button,
CircleCheckIcon,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Spinner,
XmarkIcon
} from '@noahspan/noahspan-components';
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
const ConfirmationDialog = ({
contentText,
isLoading,
isOpen,
onCancel,
onConfirm,
title
}: IDialogConfirmationProps) => {
return (
<Dialog
onClose={onCancel}
open={isOpen}
sx={{
'& .MuiDialog-paper': { width: '2000px' }
}}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ textAlign: 'center' }}>
{!isLoading && <DialogContentText>{contentText}</DialogContentText>}
{isLoading && <Spinner />}
</DialogContent>
<DialogActions>
<Button onClick={onCancel} variant="outlined" startIcon={<XmarkIcon />}>
No
</Button>
<Button
onClick={onConfirm}
variant="contained"
startIcon={<CircleCheckIcon />}
>
Yes
</Button>
</DialogActions>
</Dialog>
);
};
export default ConfirmationDialog;

View File

@@ -0,0 +1,8 @@
export interface IDialogConfirmationProps {
contentText: string;
isLoading: boolean;
isOpen: boolean;
onCancel: () => void;
onConfirm: () => void;
title: string;
}

View File

@@ -0,0 +1,27 @@
export interface ILogbookEntry {
partitionKey: string;
rowKey: string;
id: string;
pilotId: string;
date: string;
aircraftMakeModel: string;
aircraftIdentity: string;
routeFrom: string;
routeTo: string;
durationOfFlight: number | null;
singleEngineLand: number | null;
simulatorAtd: number | null;
landingsDay: number | null;
landingsNight: number | null;
instrumentActual: number | null;
instrumentSimulated: number | null;
instrumentApproaches: number | null;
instrumentHolds: number | null;
instrumentNavTrack: number | null;
groundTrainingReceived: number;
flightTrainingReceived: number;
crossCountry: number | null;
night: number | null;
solo: number | null;
pilotInCommand: number | null;
}

View File

@@ -0,0 +1,13 @@
import { FormMode } from '../../enums/formMode';
import { ILogbookEntry } from './ILogbookEntry';
export interface ILogbookState {
entries: ILogbookEntry[];
formMode: FormMode;
error: string | undefined;
isConfirmDialogLoading: boolean;
isConfirmDialogOpen: boolean;
isFormOpen: boolean;
isLoading: boolean;
selectedEntryId: string | undefined;
}

View File

@@ -1,75 +1,130 @@
import { useEffect, useState } from 'react';
import { useEffect, useReducer, useState } from 'react';
import LogbookEntryForm from '../logbookEntryForm/LogbookEntryForm';
import {
Alert,
Box,
Button,
ColumnDef,
Grid,
PlusIcon,
Spinner,
Table,
Typography
} from '@noahspan/noahspan-components';
import { initialState, reducer } from './reducer';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
import ActionMenu from '../../actionMenu/ActionMenu';
type LogbookEntry = {
partitionKey: string;
rowKey: string;
id: string;
pilotId: string;
date: string;
aircraftMakeModel: string;
aircraftIdentity: string;
routeFrom: string;
routeTo: string;
durationOfFlight: number | null;
singleEngineLand: number | null;
simulatorAtd: number | null;
landingsDay: number | null;
landingsNight: number | null;
instrumentActual: number | null;
instrumentSimulated: number | null;
instrumentApproaches: number | null;
instrumentHolds: number | null;
instrumentNavTrack: number | null;
groundTrainingReceived: number;
flightTrainingReceived: number;
crossCountry: number | null;
night: number | null;
solo: number | null;
pilotInCommand: number | null;
};
import ActionMenu from '../actionMenu/ActionMenu';
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
import { ILogbookEntry } from './ILogbookEntry';
const Logbook: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [entryFormMode, setEntryFormMode] = useState<FormMode>(FormMode.CANCEL);
const [selectedEntryId, setSelectedEntryId] = useState<string | undefined>();
const [entries, setEntries] = useState([]);
const getLogbookEntries = async () => {
try {
dispatch({ type: 'SET_IS_LOADING', payload: true });
const token = await getAccessToken();
const config = isAuthenticated
? { headers: { Authorization: `${token}` } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook`,
config
);
dispatch({ type: 'SET_ENTRIES', payload: response.data });
} catch (error) {
const axiosError = error as AxiosError;
dispatch({
type: 'SET_ERROR',
payload: `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:
setEntryFormMode(mode);
setSelectedEntryId(entryId);
setIsDrawerOpen(true);
dispatch({
type: 'SET_OPEN_CLOSE_ENTRY_FORM',
payload: {
formMode: mode,
selectedEntryId: entryId,
isFormOpen: true
}
});
break;
case FormMode.CANCEL:
setEntryFormMode(mode);
setSelectedEntryId(undefined);
setIsDrawerOpen(false);
dispatch({
type: 'SET_OPEN_CLOSE_ENTRY_FORM',
payload: {
formMode: mode,
selectedEntryId: undefined,
isFormOpen: false
}
});
break;
}
};
const columns: ColumnDef<LogbookEntry>[] = [
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/logbook/${state.selectedEntryId}`, config);
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: false, selectedEntryId: undefined }
});
await getLogbookEntries();
} catch (error) {
const axiosError = error as AxiosError;
dispatch({
type: 'SET_ERROR',
payload: `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 }
});
};
const columns: ColumnDef<ILogbookEntry>[] = [
{
accessorKey: 'pilotName',
header: 'Pilot'
@@ -303,6 +358,7 @@ const Logbook: React.FC<unknown> = () => {
cell: (info) => (
<ActionMenu
id={info.row.original.rowKey}
onDelete={onDeleteEntry}
onOpenCloseForm={onOpenCloseEntryForm}
/>
)
@@ -310,27 +366,10 @@ const Logbook: React.FC<unknown> = () => {
];
useEffect(() => {
const getLogbookEntries = async () => {
try {
const token = await getAccessToken();
const config = isAuthenticated
? { headers: { Authorization: `${token}` } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook`,
config
);
setEntries(response.data);
} catch (error) {
console.log(error);
}
};
if (isAuthenticated && !isDrawerOpen) {
if (isAuthenticated && !state.isFormOpen) {
getLogbookEntries();
}
}, [isAuthenticated, isDrawerOpen]);
}, [isAuthenticated, state.isFormOpen]);
return (
<Box sx={{ margin: '20px' }}>
@@ -348,16 +387,51 @@ const Logbook: React.FC<unknown> = () => {
Add Entry
</Button>
</Grid>
<Grid size={12}>
{entries.length > 0 && <Table columns={columns} data={entries} />}
</Grid>
{!state.isLoading && state.error && (
<Grid display="flex" justifyContent="center" size={12}>
<Alert
onClose={() =>
dispatch({ type: 'SET_ERROR', payload: undefined })
}
severity="error"
sx={{ width: '100%' }}
>
{state.error}
</Alert>
</Grid>
)}
{!state.isLoading && (
<Grid size={12}>
{state.entries.length > 0 && (
<Table columns={columns} data={state.entries} />
)}
</Grid>
)}
{state.isLoading && !state.error && (
<>
<Grid display="flex" justifyContent="center" size={12}>
<Spinner />
</Grid>
<Grid display="flex" justifyContent="center" size={12}>
Loading...
</Grid>
</>
)}
</Grid>
<LogbookEntryForm
entryId={selectedEntryId}
isDrawerOpen={isDrawerOpen}
mode={entryFormMode}
entryId={state.selectedEntryId}
isDrawerOpen={state.isFormOpen}
mode={state.formMode}
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
/>
<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"
/>
</Box>
);
};

View File

@@ -0,0 +1,92 @@
import { FormMode } from '../../enums/formMode';
import { ILogbookEntry } from './ILogbookEntry';
import { ILogbookState } from './ILogbookState';
type Action =
| {
type: 'SET_DELETE';
payload: {
isConfirmationDialogOpen: boolean;
selectedEntryId: string | undefined;
};
}
| { type: 'SET_ENTRIES'; payload: ILogbookEntry[] }
| { type: 'SET_ERROR'; payload: string | undefined }
| { type: 'SET_FORM_MODE'; payload: FormMode }
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
| { type: 'SET_IS_LOADING'; payload: boolean }
| {
type: 'SET_OPEN_CLOSE_ENTRY_FORM';
payload: {
formMode: FormMode;
selectedEntryId: string | undefined;
isFormOpen: boolean;
};
};
export const initialState: ILogbookState = {
entries: [],
error: undefined,
formMode: FormMode.CANCEL,
isConfirmDialogLoading: false,
isConfirmDialogOpen: false,
isFormOpen: false,
isLoading: false,
selectedEntryId: undefined
};
export const reducer = (
state: ILogbookState,
action: Action
): ILogbookState => {
switch (action.type) {
case 'SET_DELETE': {
return {
...state,
isConfirmDialogOpen: action.payload.isConfirmationDialogOpen,
selectedEntryId: action.payload.selectedEntryId
};
}
case 'SET_ENTRIES': {
return {
...state,
entries: action.payload
};
}
case 'SET_ERROR': {
return {
...state,
error: action.payload
};
}
case 'SET_FORM_MODE': {
return {
...state,
formMode: action.payload
};
}
case 'SET_IS_CONFIRMATION_DIALOG_LOADING': {
return {
...state,
isConfirmDialogLoading: action.payload
};
}
case 'SET_IS_LOADING': {
return {
...state,
isLoading: action.payload
};
}
case 'SET_OPEN_CLOSE_ENTRY_FORM': {
return {
...state,
formMode: action.payload.formMode,
isFormOpen: action.payload.isFormOpen,
selectedEntryId: action.payload.selectedEntryId
};
}
default: {
return state;
}
}
};

View File

@@ -1,4 +1,5 @@
export interface ILogbookEntryFormState {
error: string | undefined;
isDisabled: boolean;
isLoading: boolean;
pilotOptions: { label: string; value: string }[];

View File

@@ -3,6 +3,7 @@ import {
Accordion,
AccordionDetails,
AccordionSummary,
Alert,
Button,
ChevronDownIcon,
DatePicker,
@@ -18,7 +19,7 @@ import {
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { ILogbookEntryFormProps } from './ILogbookEntryFormProps';
import { initialState, reducer } from './reducer';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
@@ -96,12 +97,9 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
dispatch({ type: 'SET_IS_DISABLED', payload: false });
onOpenClose(FormMode.CANCEL);
} catch (error) {
if (axios.isAxiosError(error)) {
const errResp = error.response;
const axiosError = error as AxiosError;
console.log(errResp);
} else {
}
dispatch({ type: 'SET_ERROR', payload: axiosError.message });
} finally {
dispatch({ type: 'SET_IS_LOADING', payload: false });
}
@@ -129,13 +127,15 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
methods.reset(entry);
} catch (error) {
console.log(error);
const axiosError = error as AxiosError;
dispatch({ type: 'SET_ERROR', payload: axiosError.message });
} finally {
dispatch({ type: 'SET_IS_LOADING', payload: false });
}
};
if (entryId) {
if (entryId && isDrawerOpen) {
getEntry();
}
}, [entryId]);
@@ -175,6 +175,19 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
<XmarkIcon />
</IconButton>
</Grid>
{state.error && (
<Grid display="flex" justifyContent="center" size={12}>
<Alert
onClose={() =>
dispatch({ type: 'SET_ERROR', payload: undefined })
}
severity="error"
sx={{ width: '100%' }}
>
{state.error}
</Alert>
</Grid>
)}
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Pilot *</Typography>
</Grid>
@@ -200,7 +213,17 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
<Select
disabled={state.isDisabled}
fullWidth
onChange={onChange}
onChange={(event) => {
const pilot = pilots?.find(
(pilot) => (pilot.id = event.target.value)
);
if (pilot) {
methods.setValue('pilotName', pilot.name);
}
methods.setValue('pilotId', event.target.value);
}}
options={
state.pilotOptions && state.pilotOptions.length > 0
? state.pilotOptions

View File

@@ -1,12 +1,14 @@
import { ILogbookEntryFormState } from './ILogbookEntryFormState';
type Action =
| { type: 'SET_ERROR'; payload: string | undefined }
| { type: 'SET_IS_DISABLED'; payload: boolean }
| { type: 'SET_IS_LOADING'; payload: boolean }
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
| { type: 'SET_SELECTED_ENTRY_PILOT_NAME'; payload: string };
export const initialState: ILogbookEntryFormState = {
error: undefined,
isDisabled: false,
isLoading: true,
pilotOptions: [],
@@ -18,6 +20,12 @@ export const reducer = (
action: Action
): ILogbookEntryFormState => {
switch (action.type) {
case 'SET_ERROR': {
return {
...state,
error: action.payload
};
}
case 'SET_IS_DISABLED': {
return {
...state,
@@ -42,5 +50,8 @@ export const reducer = (
selectedEntryPilotName: action.payload
};
}
default: {
return state;
}
}
};