From b10bd7b252e9b13fa68c9e6322b28dca521df604 Mon Sep 17 00:00:00 2001 From: noahspannbauer Date: Wed, 20 Nov 2024 02:35:56 +0000 Subject: [PATCH] adding logbook entry delete (#31) --- api/src/logbook/logbook.controller.ts | 14 ++ api/src/logbook/logbook.entity.ts | 30 +-- api/src/logbook/logbook.service.ts | 34 ++- .../actionMenu/ActionMenu.tsx | 6 +- .../actionMenu/IActionMenuProps.tsx | 3 +- .../confirmationDialog/ConfirmationDialog.tsx | 52 +++++ .../IConfirmationDialogProps.ts | 8 + app/src/components/logbook/ILogbookEntry.ts | 27 +++ app/src/components/logbook/ILogbookState.ts | 13 ++ app/src/components/logbook/Logbook.tsx | 208 ++++++++++++------ app/src/components/logbook/reducer.ts | 92 ++++++++ .../ILogbookEntryFormState.ts | 1 + .../logbookEntryForm/LogbookEntryForm.tsx | 41 +++- .../components/logbookEntryForm/reducer.tsx | 11 + 14 files changed, 437 insertions(+), 103 deletions(-) rename app/src/{ => components}/actionMenu/ActionMenu.tsx (89%) rename app/src/{ => components}/actionMenu/IActionMenuProps.tsx (56%) create mode 100644 app/src/components/confirmationDialog/ConfirmationDialog.tsx create mode 100644 app/src/components/confirmationDialog/IConfirmationDialogProps.ts create mode 100644 app/src/components/logbook/ILogbookEntry.ts create mode 100644 app/src/components/logbook/ILogbookState.ts create mode 100644 app/src/components/logbook/reducer.ts diff --git a/api/src/logbook/logbook.controller.ts b/api/src/logbook/logbook.controller.ts index 5d9f3a9..b01fd29 100644 --- a/api/src/logbook/logbook.controller.ts +++ b/api/src/logbook/logbook.controller.ts @@ -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 { + try { + await this.logbookService.delete(params.rowKey); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode, { + cause: customError.name + }); + } + } } diff --git a/api/src/logbook/logbook.entity.ts b/api/src/logbook/logbook.entity.ts index 479e423..619ca54 100644 --- a/api/src/logbook/logbook.entity.ts +++ b/api/src/logbook/logbook.entity.ts @@ -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; } diff --git a/api/src/logbook/logbook.service.ts b/api/src/logbook/logbook.service.ts index a0a203e..6819c67 100644 --- a/api/src/logbook/logbook.service.ts +++ b/api/src/logbook/logbook.service.ts @@ -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 { - 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 { + 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 + ); + } + } } diff --git a/app/src/actionMenu/ActionMenu.tsx b/app/src/components/actionMenu/ActionMenu.tsx similarity index 89% rename from app/src/actionMenu/ActionMenu.tsx rename to app/src/components/actionMenu/ActionMenu.tsx index 2c89f1c..6bf8df3 100644 --- a/app/src/actionMenu/ActionMenu.tsx +++ b/app/src/components/actionMenu/ActionMenu.tsx @@ -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 ); @@ -50,7 +50,7 @@ const ActionMenu = ({ id, onOpenCloseForm }: IActionMenuProps) => { View
- + onDelete(id)}> diff --git a/app/src/actionMenu/IActionMenuProps.tsx b/app/src/components/actionMenu/IActionMenuProps.tsx similarity index 56% rename from app/src/actionMenu/IActionMenuProps.tsx rename to app/src/components/actionMenu/IActionMenuProps.tsx index 11af323..bd5f036 100644 --- a/app/src/actionMenu/IActionMenuProps.tsx +++ b/app/src/components/actionMenu/IActionMenuProps.tsx @@ -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; } diff --git a/app/src/components/confirmationDialog/ConfirmationDialog.tsx b/app/src/components/confirmationDialog/ConfirmationDialog.tsx new file mode 100644 index 0000000..9607489 --- /dev/null +++ b/app/src/components/confirmationDialog/ConfirmationDialog.tsx @@ -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 ( + + {title} + + {!isLoading && {contentText}} + {isLoading && } + + + + + + + ); +}; + +export default ConfirmationDialog; diff --git a/app/src/components/confirmationDialog/IConfirmationDialogProps.ts b/app/src/components/confirmationDialog/IConfirmationDialogProps.ts new file mode 100644 index 0000000..90cd809 --- /dev/null +++ b/app/src/components/confirmationDialog/IConfirmationDialogProps.ts @@ -0,0 +1,8 @@ +export interface IDialogConfirmationProps { + contentText: string; + isLoading: boolean; + isOpen: boolean; + onCancel: () => void; + onConfirm: () => void; + title: string; +} diff --git a/app/src/components/logbook/ILogbookEntry.ts b/app/src/components/logbook/ILogbookEntry.ts new file mode 100644 index 0000000..a410e3f --- /dev/null +++ b/app/src/components/logbook/ILogbookEntry.ts @@ -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; +} diff --git a/app/src/components/logbook/ILogbookState.ts b/app/src/components/logbook/ILogbookState.ts new file mode 100644 index 0000000..f28b73d --- /dev/null +++ b/app/src/components/logbook/ILogbookState.ts @@ -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; +} diff --git a/app/src/components/logbook/Logbook.tsx b/app/src/components/logbook/Logbook.tsx index f232e1b..ea69897 100644 --- a/app/src/components/logbook/Logbook.tsx +++ b/app/src/components/logbook/Logbook.tsx @@ -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 = () => { + 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.CANCEL); - const [selectedEntryId, setSelectedEntryId] = useState(); - 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[] = [ + 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[] = [ { accessorKey: 'pilotName', header: 'Pilot' @@ -303,6 +358,7 @@ const Logbook: React.FC = () => { cell: (info) => ( ) @@ -310,27 +366,10 @@ const Logbook: React.FC = () => { ]; 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 ( @@ -348,16 +387,51 @@ const Logbook: React.FC = () => { Add Entry - - {entries.length > 0 && } - + {!state.isLoading && state.error && ( + + + dispatch({ type: 'SET_ERROR', payload: undefined }) + } + severity="error" + sx={{ width: '100%' }} + > + {state.error} + + + )} + {!state.isLoading && ( + + {state.entries.length > 0 && ( +
+ )} + + )} + {state.isLoading && !state.error && ( + <> + + + + + Loading... + + + )} onOpenCloseEntryForm(mode)} /> + ); }; diff --git a/app/src/components/logbook/reducer.ts b/app/src/components/logbook/reducer.ts new file mode 100644 index 0000000..d676447 --- /dev/null +++ b/app/src/components/logbook/reducer.ts @@ -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; + } + } +}; diff --git a/app/src/components/logbookEntryForm/ILogbookEntryFormState.ts b/app/src/components/logbookEntryForm/ILogbookEntryFormState.ts index 3161d06..6a5f7e2 100644 --- a/app/src/components/logbookEntryForm/ILogbookEntryFormState.ts +++ b/app/src/components/logbookEntryForm/ILogbookEntryFormState.ts @@ -1,4 +1,5 @@ export interface ILogbookEntryFormState { + error: string | undefined; isDisabled: boolean; isLoading: boolean; pilotOptions: { label: string; value: string }[]; diff --git a/app/src/components/logbookEntryForm/LogbookEntryForm.tsx b/app/src/components/logbookEntryForm/LogbookEntryForm.tsx index 029e979..538a697 100644 --- a/app/src/components/logbookEntryForm/LogbookEntryForm.tsx +++ b/app/src/components/logbookEntryForm/LogbookEntryForm.tsx @@ -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 = ({ 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 = ({ 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 = ({ + {state.error && ( + + + dispatch({ type: 'SET_ERROR', payload: undefined }) + } + severity="error" + sx={{ width: '100%' }} + > + {state.error} + + + )} Pilot * @@ -200,7 +213,17 @@ const LogbookEntryForm: React.FC = ({