adding pilot delete (#41)

* adding pilot delete

* adding pilot delete
This commit was merged in pull request #41.
This commit is contained in:
2025-02-05 00:38:43 +00:00
committed by GitHub
parent fd28bbfd20
commit b9629ee4e9
7 changed files with 254 additions and 103 deletions

View File

@@ -4,9 +4,23 @@ import { PilotService } from './pilot.service';
import { AzureTableStorageModule } from '@noahspan/azure-database'; import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Pilot } from './pilot.entity'; import { Pilot } from './pilot.entity';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { Log } from 'src/log/log.entity';
@Module({ @Module({
imports: [ imports: [
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Log, {
createTableIfNotExists: false,
table: 'logs'
}),
AzureTableStorageModule.forRootAsync({ AzureTableStorageModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => { useFactory: async (configService: ConfigService) => {
@@ -19,7 +33,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
AzureTableStorageModule.forFeature(Pilot, { AzureTableStorageModule.forFeature(Pilot, {
createTableIfNotExists: false, createTableIfNotExists: false,
table: 'pilots' table: 'pilots'
}), })
], ],
controllers: [PilotController], controllers: [PilotController],
providers: [PilotService] providers: [PilotService]

View File

@@ -1,12 +1,14 @@
import { InjectRepository, Repository } from '@noahspan/azure-database'; import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Pilot } from './pilot.entity'; import { Pilot } from './pilot.entity';
import { Log } from 'src/log/log.entity';
import { LogService } from 'src/log/log.service';
@Injectable() @Injectable()
export class PilotService { export class PilotService {
constructor( constructor(
@InjectRepository(Pilot) @InjectRepository(Pilot) private readonly pilotRepository: Repository<Pilot>,
private readonly pilotRepository: Repository<Pilot> @InjectRepository(Log) private readonly logRepository: Repository<Log>
) {} ) {}
async find(partitionKey: string, rowKey: string): Promise<Pilot> { async find(partitionKey: string, rowKey: string): Promise<Pilot> {
@@ -35,6 +37,18 @@ export class PilotService {
} }
async delete(partitionKey: string, rowKey: string): Promise<void> { async delete(partitionKey: string, rowKey: string): Promise<void> {
const pilotLogs: Log[] = await this.logRepository.findAll({
queryOptions: {
filter: `pilotId eq '${rowKey}'`
}
})
for (const pilotLog of pilotLogs) {
await this.logRepository.delete(pilotLog.partitionKey, pilotLog.rowKey);
}
await this.pilotRepository.delete(partitionKey, rowKey); await this.pilotRepository.delete(partitionKey, rowKey);
return
} }
} }

View File

@@ -375,14 +375,16 @@ const Logbook: React.FC<unknown> = () => {
<Typography variant="h4">Logbook</Typography> <Typography variant="h4">Logbook</Typography>
</Grid> </Grid>
<Grid display="flex" justifyContent="right" size={1}> <Grid display="flex" justifyContent="right" size={1}>
<Button {isAuthenticated &&
onClick={() => onOpenCloseEntryForm(FormMode.ADD)} <Button
startIcon={<PlusIcon />} onClick={() => onOpenCloseEntryForm(FormMode.ADD)}
variant="contained" startIcon={<PlusIcon />}
data-testid="pilot-add-button" variant="contained"
> data-testid="pilot-add-button"
Add Entry >
</Button> Add Entry
</Button>
}
</Grid> </Grid>
{!state.isLoading && state.error && ( {!state.isLoading && state.error && (
<Grid display="flex" justifyContent="center" size={12}> <Grid display="flex" justifyContent="center" size={12}>

View File

@@ -0,0 +1,6 @@
export interface Pilot {
partitionKey: string;
rowKey: string;
id: string;
name: string;
};

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useReducer, useState } from 'react';
import PilotForm from '../pilotForm/PilotForm'; import PilotForm from '../pilotForm/PilotForm';
import { import {
Box, Box,
@@ -19,26 +19,20 @@ import {
Typography Typography
} from '@noahspan/noahspan-components'; } from '@noahspan/noahspan-components';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios'; import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react'; import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode'; import { FormMode } from '../../enums/formMode';
import { Pilot } from './Pilot.interface';
type Pilot = { import { initialState, reducer } from './reducer';
partitionKey: string; import ActionMenu from '../actionMenu/ActionMenu';
rowKey: string; import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
id: string;
name: string;
};
const Pilots: React.FC<unknown> = () => { const Pilots: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const httpClient: AxiosInstance = useHttpClient(); const httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken(); const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [pilotFormMode, setPilotFormMode] = useState<FormMode>(FormMode.CANCEL);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]);
const getPilots = async (): Promise<Pilot[]> => { const getPilots = async (): Promise<Pilot[]> => {
try { try {
@@ -62,76 +56,70 @@ const Pilots: React.FC<unknown> = () => {
case FormMode.ADD: case FormMode.ADD:
case FormMode.EDIT: case FormMode.EDIT:
case FormMode.VIEW: case FormMode.VIEW:
setPilotFormMode(mode); dispatch({
setSelectedPilotId(pilotId); type: 'SET_OPEN_CLOSE_ENTRY_FORM',
setIsDrawerOpen(true); payload: {
formMode: mode,
selectedPilotId: pilotId,
isFormOpen: true
}
})
break; break;
case FormMode.CANCEL: case FormMode.CANCEL:
const pilots = await getPilots(); dispatch({
type: 'SET_OPEN_CLOSE_ENTRY_FORM',
payload: {
formMode: mode,
selectedPilotId: undefined,
isFormOpen: false
}
})
setPilots(pilots);
setPilotFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
break; break;
} }
}; };
interface ActionMenuProps { const onDeleteEntry = (pilotId: string) => {
pilotId: string; dispatch({
} type: 'SET_DELETE',
payload: { isConfirmDialogOpen: true, selectedPilotId: pilotId }
});
};
const ActionMenu = ({ pilotId }: ActionMenuProps) => { const onConfirmationDialogConfirm = async () => {
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>( try {
null dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
);
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => { const token = await getAccessToken();
console.log(event); const config = isAuthenticated
setAnchorElAction(event.currentTarget); ? { headers: { Authorization: `${token}` } }
}; : {};
const onCloseActionMenu = () => { await httpClient.delete(`api/pilots/pilot/${state.selectedPilotId}`, config);
setAnchorElAction(null);
};
return ( dispatch({
<div> type: 'SET_DELETE',
<IconButton onClick={onOpenActionMenu}> payload: { isConfirmDialogOpen: false, selectedPilotId: undefined }
<EllipsisVerticalIcon size="sm" /> });
</IconButton> await getPilots();
<Menu } catch (error) {
anchorEl={anchorElAction} const axiosError = error as AxiosError;
keepMounted
open={Boolean(anchorElAction)} dispatch({
onClose={onCloseActionMenu} type: 'SET_ERROR',
> payload: `Loading of logbook entries failed with the following message: ${axiosError.message}`
<MenuItem });
onClick={() => onOpenClosePilotForm(FormMode.EDIT, pilotId)} } finally {
> dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
<ListItemIcon> }
<PenIcon size="lg" /> };
</ListItemIcon>
<ListItemText>Edit</ListItemText> const onConfirmationDialogCancel = () => {
</MenuItem> dispatch({
<MenuItem type: 'SET_DELETE',
onClick={() => onOpenClosePilotForm(FormMode.VIEW, pilotId)} payload: { isConfirmDialogOpen: false, selectedPilotId: undefined }
> });
<ListItemIcon>
<EyeIcon size="lg" />
</ListItemIcon>
<ListItemText>View</ListItemText>
</MenuItem>
<hr className="my-3" />
<MenuItem>
<ListItemIcon>
<TrashIcon size="lg" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</Menu>
</div>
);
}; };
const columns: ColumnDef<Pilot>[] = [ const columns: ColumnDef<Pilot>[] = [
@@ -141,7 +129,13 @@ const Pilots: React.FC<unknown> = () => {
}, },
{ {
header: 'Actions', header: 'Actions',
cell: (info) => <ActionMenu pilotId={info.row.original.rowKey} /> cell: (info) => (
<ActionMenu
id={info.row.original.rowKey}
onDelete={onDeleteEntry}
onOpenCloseForm={onOpenClosePilotForm}
/>
)
} }
]; ];
@@ -150,14 +144,16 @@ const Pilots: React.FC<unknown> = () => {
try { try {
const pilots = await getPilots(); const pilots = await getPilots();
setPilots(pilots); dispatch({ type: 'SET_PILOTS', payload: pilots })
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
}; };
loadPilots(); if (isAuthenticated && !state.isFormOpen) {
}, []); loadPilots();
}
}, [isAuthenticated, state.isFormOpen]);
return ( return (
<Box sx={{ margin: '20px' }}> <Box sx={{ margin: '20px' }}>
@@ -166,25 +162,39 @@ const Pilots: React.FC<unknown> = () => {
<Typography variant="h4">Pilots</Typography> <Typography variant="h4">Pilots</Typography>
</Grid> </Grid>
<Grid display="flex" justifyContent="right" size={1}> <Grid display="flex" justifyContent="right" size={1}>
<Button {isAuthenticated &&
onClick={() => onOpenClosePilotForm(FormMode.ADD)} <Button
startIcon={<PlusIcon />} onClick={() => onOpenClosePilotForm(FormMode.ADD)}
variant="contained" startIcon={<PlusIcon />}
data-testid="pilot-add-button" variant="contained"
> data-testid="pilot-add-button"
Add Pilot >
</Button> Add Pilot
</Button>
}
</Grid> </Grid>
<Grid size={12}> <Grid size={12}>
{pilots.length > 0 && <Table columns={columns} data={pilots} />} {state.pilots.length > 0 && <Table columns={columns} data={state.pilots} />}
</Grid> </Grid>
</Grid> </Grid>
<PilotForm {state.isFormOpen && (
isDrawerOpen={isDrawerOpen} <PilotForm
mode={pilotFormMode} isDrawerOpen={state.isFormOpen}
onOpenClose={(mode) => onOpenClosePilotForm(mode)} mode={state.formMode}
pilotId={selectedPilotId} onOpenClose={(mode) => onOpenClosePilotForm(mode)}
/> pilotId={state.selectedPilotId}
/>
)}
{state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete the pilot entry? Deleting a pilot will delete the pilot and delete all logbook entries for the pilot."
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmationDialogCancel}
onConfirm={onConfirmationDialogConfirm}
title="Confirm Delete"
/>
)}
</Box> </Box>
); );
}; };

View File

@@ -0,0 +1,13 @@
import { FormMode } from "../../enums/formMode";
import { Pilot } from "./Pilot.interface";
export interface PilotsState {
pilots: Pilot[];
error: string | undefined;
isConfirmDialogLoading: boolean;
isConfirmDialogOpen: boolean;
isFormOpen: boolean;
isLoading: boolean;
formMode: FormMode;
selectedPilotId: string | undefined;
}

View File

@@ -0,0 +1,92 @@
import { FormMode } from "../../enums/formMode";
import { Pilot } from "./Pilot.interface";
import { PilotsState } from './PilotsState.interface'
type Action =
| {
type: 'SET_DELETE';
payload: {
isConfirmDialogOpen: boolean;
selectedPilotId: string | undefined;
}
}
| { type: 'SET_PILOTS'; payload: Pilot[] }
| { 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;
selectedPilotId: string | undefined;
isFormOpen: boolean;
}
}
export const initialState: PilotsState = {
pilots: [],
error: undefined,
formMode: FormMode.CANCEL,
isConfirmDialogLoading: false,
isConfirmDialogOpen: false,
isFormOpen: false,
isLoading: false,
selectedPilotId: undefined
}
export const reducer = (
state: PilotsState,
action: Action
): PilotsState => {
switch (action.type) {
case 'SET_DELETE': {
return {
...state,
isConfirmDialogOpen: action.payload.isConfirmDialogOpen,
selectedPilotId: action.payload.selectedPilotId
}
}
case 'SET_PILOTS': {
return {
...state,
pilots: 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,
selectedPilotId: action.payload.selectedPilotId
}
}
default: {
return state;
}
}
}