adding new logbook entry

This commit is contained in:
2024-11-17 07:06:10 -06:00
parent 11e00b5b14
commit d379ecfdcf
27 changed files with 5960 additions and 824 deletions

2
api/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
test

50
api/Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice
# FROM mcr.microsoft.com/azure-functions/node:4-node18
# ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
# AzureFunctionsJobHost__Logging__Console__IsEnabled=true
# COPY . /home/site/wwwroot
# RUN cd /home/site/wwwroot && \
# npm install
# EXPOSE 7071
# CMD ["npm", "run", "start:azure"]
# FROM mcr.microsoft.com/azure-functions/node:4-node18 AS build
# WORKDIR /home/site/wwwroot
# COPY . .
# RUN npm install && \
# npm run build
FROM mcr.microsoft.com/azure-functions/node:4-node18
WORKDIR /home/site/wwwroot
COPY . .
RUN npm install -g azure-functions-core-tools@4 --unsafe-perm true
RUN npm install
# ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
# AzureFunctionsJobHost__Logging__Console__IsEnabled=true
# AzureFunctionsJobHost__extensions__cors__allowedOrigins="[\"*\"]" \
# AzureFunctionsJobHost__extensions__cors__supportCredentials=false
# COPY --from=build /home/site/wwwroot/node_modules /home/site/wwwroot/node_modules
# COPY --from=build /home/site/wwwroot/dist /home/site/wwwroot/dist
# EXPOSE 3000
# CMD ["node", "/home/site/wwwroot/dist/src/main.js"]
CMD ["npm", "run", "start:azure"]

View File

@@ -30,11 +30,12 @@
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@noahspan/noahspan-modules": "^0.4.0", "@noahspan/noahspan-modules": "^0.4.7",
"@schematics/angular": "^17.3.7", "@schematics/angular": "^17.3.7",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"uuid": "^10.0.0"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/microsoft-graph-types": "^2.40.0", "@microsoft/microsoft-graph-types": "^2.40.0",

View File

@@ -1,4 +0,0 @@
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {}
}

View File

@@ -9,6 +9,7 @@ import {
} from '@noahspan/noahspan-modules'; } from '@noahspan/noahspan-modules';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core'; import { APP_GUARD } from '@nestjs/core';
import { LogbookModule } from './logbook/logbook.module';
import { PilotModule } from './pilot/pilot.module'; import { PilotModule } from './pilot/pilot.module';
import { APP_FILTER } from '@nestjs/core'; import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter'; import { HttpExceptionFilter } from './filters/http-exception.filter';
@@ -32,6 +33,7 @@ import { HttpExceptionFilter } from './filters/http-exception.filter';
clientId: process.env.CLIENT_ID, clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET clientSecret: process.env.CLIENT_SECRET
}), }),
LogbookModule,
PilotModule PilotModule
], ],
controllers: [AppController], controllers: [AppController],

View File

@@ -0,0 +1,41 @@
import { Body, Controller, Get, HttpException, Post } from '@nestjs/common';
import { LogbookService } from './logbook.service';
import { LogbookDto } from './logbook.dto';
import { CustomError } from '../customError/CustomError';
import { TableInsertEntityHeaders } from '@azure/data-tables';
import { LogbookEntity } from './logbook.entity';
@Controller('logbook')
export class LogbookController {
constructor(private readonly logbookService: LogbookService) {}
@Get()
async findAll(): Promise<LogbookEntity[]> {
try {
const logbookEntries: LogbookEntity[] =
await this.logbookService.findAll();
return logbookEntries;
} catch (error) {
const customError = error as CustomError;
console.log(error);
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
@Post()
async create(@Body() logbookData: LogbookDto): Promise<void> {
try {
const response: TableInsertEntityHeaders =
await this.logbookService.create(logbookData);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
}

View File

@@ -0,0 +1,26 @@
export class LogbookDto {
pilotId: string;
pilotName: string;
date: string;
aircraftMakeModel: string;
aircraftIdentity: string;
routeFrom: string;
routeTo: string;
durationOfFlight: number;
singleEngineLand: string;
simulatorAtd: number;
landingsDay: number;
landingsNight: number;
instrumentActual: number;
instrumentSimulated: number;
instrumentApproaches: number;
instrumentHolds: number;
instrumentNavTrack: number;
groundTrainingReceived: number;
flightTrainingReceived: number;
crossCountry: number;
night: number;
solo: number;
pilotInCommand: number;
notes: string;
}

View File

@@ -0,0 +1,29 @@
export class LogbookEntity {
partitionKey: string;
rowKey: string;
id: string;
pilotId: string;
pilotName: 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;
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

@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { LogbookController } from './logbook.controller';
import { LogbookService } from './logbook.service';
import { TableModule } from '@noahspan/noahspan-modules';
@Module({
imports: [
TableModule.register({
accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME,
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
accountUrl: process.env.AZURE_STORAGE_ACCOUNT_URL,
allowInsecureConnection: Boolean(
process.env.AZURE_STORAGE_ALLOW_INSECURE_CONNECTION
)
})
],
controllers: [LogbookController],
providers: [LogbookService]
})
export class LogbookModule {}

View File

@@ -0,0 +1,113 @@
import { Injectable } from '@nestjs/common';
import { TableClient, TableService } from '@noahspan/noahspan-modules';
import { LogbookDto } from './logbook.dto';
import { LogbookEntity } from './logbook.entity';
import { odata, RestError, TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../customError/CustomError';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class LogbookService {
constructor(private readonly tableService: TableService) {}
async findAll(): Promise<LogbookEntity[]> {
try {
const client: TableClient =
await this.tableService.getTableClient('Logbook');
const entities = await client.listEntities({
queryOptions: { filter: odata`PartitionKey eq 'entry'` }
});
const logbookEntries: LogbookEntity[] = [];
for await (const entity of entities) {
const logbookEntry = {
partitionKey: entity.partitionKey.toString(),
rowKey: entity.rowKey.toString(),
id: entity.rowKey.toString(),
pilotId: entity.pilotId.toString(),
pilotName: entity.pilotName.toString(),
date: entity.date.toString(),
aircraftMakeModel: entity.aircraftMakeModel.toString(),
aircraftIdentity: entity.aircraftIdentity.toString(),
routeFrom: entity.routeFrom.toString(),
routeTo: entity.routeTo.toString(),
durationOfFlight: Number(entity.durationOfFlight),
singleEngineLand: entity.singleEngineLand
? Number(entity.singleEngineLand)
: null,
simulatorAtd: entity.simulatorAtd
? Number(entity.simulatorAtd)
: null,
landingsDay: entity.landingsDay ? Number(entity.landingsDay) : null,
landingsNight: entity.landingsNight
? Number(entity.landingsNight)
: null,
groundTrainingReceived: entity.groundTrainingReceived
? Number(entity.groundTrainingReceived)
: null,
flightTrainingReceived: entity.flightTrainingReceived
? Number(entity.flightTrainingReceived)
: null,
crossCountry: entity.crossCountry
? Number(entity.crossCountry)
: null,
night: entity.night ? Number(entity.night) : null,
solo: entity.solo ? Number(entity.solo) : null,
pilotInCommand: entity.pilotInCommand
? Number(entity.pilotInCommand)
: null,
instrumentActual: entity.instrumentActual
? Number(entity.instrumentActual)
: null,
instrumentSimulated: entity.instrumentSimulated
? Number(entity.instrumentSimulated)
: null,
instrumentApproaches: entity.instrumentApproaches
? Number(entity.instrumentApproaches)
: null,
instrumentHolds: entity.instrumentHolds
? Number(entity.instrumentHolds)
: null,
instrumentNavTrack: entity.instrumentNavTrack
? Number(entity.instrumentNavTrack)
: null,
notes: entity.notes.toString()
};
console.log(logbookEntry);
logbookEntries.push(logbookEntry);
}
return logbookEntries;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async create(logbookData: LogbookDto): Promise<TableInsertEntityHeaders> {
const client: TableClient =
await this.tableService.getTableClient('Logbook');
const logbook: LogbookEntity = new LogbookEntity();
Object.assign(logbook, logbookData);
logbook.partitionKey = 'entry';
logbook.rowKey = `${logbook.pilotId}:${uuidv4()}`;
try {
return await client.createEntity(logbook);
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
}

View File

@@ -82,19 +82,19 @@ export class PilotInfoService {
} }
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> { async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
const pilotInfo: PilotInfoEntity = new PilotInfoEntity();
Object.assign(pilotInfo, pilotInfoData);
pilotInfo.partitionKey = 'pilot';
pilotInfo.rowKey = pilotInfo.id;
try { try {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
const pilotInfo: PilotInfoEntity = new PilotInfoEntity();
Object.assign(pilotInfo, pilotInfoData);
pilotInfo.partitionKey = 'pilot';
pilotInfo.rowKey = pilotInfo.id;
console.log(pilotInfo);
return await client.createEntity(pilotInfo); return await client.createEntity(pilotInfo);
} catch (error) { } catch (error) {
const restError: RestError = error as RestError; const restError: RestError = error as RestError;
console.log(restError);
throw new CustomError( throw new CustomError(
restError.details['odataError']['message']['value'], restError.details['odataError']['message']['value'],
restError.details['odataError']['code'], restError.details['odataError']['code'],

View File

@@ -7,7 +7,11 @@ import { PilotInfoService } from './info/pilot-info.service';
imports: [ imports: [
TableModule.register({ TableModule.register({
accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME, accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME,
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
accountUrl: process.env.AZURE_STORAGE_ACCOUNT_URL,
allowInsecureConnection: Boolean(
process.env.AZURE_STORAGE_ALLOW_INSECURE_CONNECTION
)
}) })
], ],
controllers: [PilotController], controllers: [PilotController],

View File

@@ -16,7 +16,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.7.0", "@noahspan/noahspan-components": "^0.8.9",
"axios": "^1.7.2", "axios": "^1.7.2",
"framer-motion": "^11.1.7", "framer-motion": "^11.1.7",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import Pilots from './components/pilots/Pilots'; import Pilots from './components/pilots/Pilots';
import Logbook from './components/logbook/Logbook';
import { useAppContext } from './hooks/appContext/UseAppContext'; import { useAppContext } from './hooks/appContext/UseAppContext';
import { AxiosInstance, AxiosResponse } from 'axios'; import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from './hooks/httpClient/UseHttpClient'; import { useHttpClient } from './hooks/httpClient/UseHttpClient';
@@ -35,12 +36,13 @@ const App: React.FC<unknown> = () => {
}, []); }, []);
return ( return (
<div className="container mx-auto"> <div>
<SiteNav /> <SiteNav />
<Routes> <Routes>
{useFeatureFlag('flying-pilots')?.enabled && ( {useFeatureFlag('flying-pilots')?.enabled && (
<Route path="/" element={<Pilots />} /> <Route path="/pilots" element={<Pilots />} />
)} )}
<Route path="/" element={<Logbook />} />
</Routes> </Routes>
</div> </div>
); );

View File

@@ -1,8 +1,351 @@
import { useEffect, useState } from 'react';
import LogbookEntryForm from '../logbookEntryForm/LogbookEntryForm';
import {
Box,
Button,
ColumnDef,
Grid,
PlusIcon,
Table,
Typography
} from '@noahspan/noahspan-components';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
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;
};
const Logbook: React.FC<unknown> = () => { const Logbook: React.FC<unknown> = () => {
const httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [entryFormMode, setEntryFormMode] = useState<FormMode>(FormMode.CANCEL);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [entries, setEntries] = useState([]);
const onOpenCloseEntryForm = (mode: FormMode, pilotId?: string) => {
switch (mode) {
case FormMode.ADD:
case FormMode.EDIT:
case FormMode.VIEW:
setEntryFormMode(mode);
setSelectedPilotId(pilotId);
setIsDrawerOpen(true);
break;
case FormMode.CANCEL:
setEntryFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
break;
}
};
const columns: ColumnDef<LogbookEntry>[] = [
{
accessorKey: 'pilotName',
header: 'Pilot'
},
{
accessorKey: 'date',
header: 'Date'
},
{
accessorKey: 'aircraftMakeModel',
header: 'Aircraft Make & Model'
},
{
accessorKey: 'aircraftIdentity',
header: 'Aircraft Identity'
},
{
id: 'route',
header: 'Route of Flight',
meta: {
headerAlign: 'center'
},
columns: [
{
accessorKey: 'routeFrom',
header: 'From'
},
{
accessorKey: 'routeTo',
header: 'To'
}
]
},
{
accessorKey: 'durationOfFlight',
header: 'Duration Of Flight',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()!.toString()).toFixed(1)
: ''
},
{
accessorKey: 'singleEngineLand',
header: 'Single Engine Land',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()!.toString()).toFixed(1)
: null
},
{
id: 'landings',
header: 'Landings',
meta: {
headerAlign: 'center'
},
columns: [
{
accessorKey: 'landingsDay',
header: 'Day',
meta: {
align: 'right',
headerAlign: 'right'
}
},
{
accessorKey: 'landingsNight',
header: 'Night',
meta: {
align: 'right',
headerAlign: 'right'
}
}
]
},
{
id: 'instrument',
header: 'Instrument',
meta: {
headerAlign: 'center'
},
columns: [
{
accessorKey: 'instrumentActual',
header: 'Actual',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'instrumentSimulated',
header: 'Simulated',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'instrumentApproaches',
header: 'Approaches',
meta: {
align: 'right',
headerAlign: 'right'
}
},
{
accessorKey: 'instrumentHolds',
header: 'Holds',
meta: {
align: 'right',
headerAlign: 'right'
}
},
{
accessorKey: 'instrumentNavTrack',
header: 'Nav/Track',
meta: {
align: 'right',
headerAlign: 'right'
}
}
]
},
{
id: 'experienceTraining',
header: 'Type of pilot experience or training',
meta: {
headerAlign: 'center'
},
columns: [
{
accessorKey: 'groundTrainingReceived',
header: 'Ground Training Received',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'flightTrainingReceived',
header: 'Flight Training Received',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'crossCountry',
header: 'Cross Country',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'night',
header: 'Night',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'solo',
header: 'Solo',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
},
{
accessorKey: 'pilotInCommand',
header: 'Pilot In Command',
meta: {
align: 'right',
headerAlign: 'right'
},
cell: (info) =>
info.getValue() !== null
? parseFloat(info.getValue()).toFixed(1)
: ''
}
]
},
{
accessorKey: 'notes',
header: 'Notes'
}
];
useEffect(() => {
const getLogbookEntries = async () => {
try {
const token = await getAccessToken();
const config = isAuthenticated
? { headers: { Authorization: `${token}` } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook`,
config
);
console.log(response);
setEntries(response.data);
} catch (error) {
console.log(error);
}
};
if (isAuthenticated && !isDrawerOpen) {
getLogbookEntries();
}
}, [isAuthenticated, isDrawerOpen]);
return ( return (
<div> <Box sx={{ margin: '20px' }}>
<div>Logbook goes here</div> <Grid container spacing={2}>
</div> <Grid size={11}>
<Typography variant="h4">Logbook</Typography>
</Grid>
<Grid display="flex" justifyContent="right" size={1}>
<Button
onClick={() => onOpenCloseEntryForm(FormMode.ADD)}
startIcon={<PlusIcon />}
variant="contained"
data-testid="pilot-add-button"
>
Add Entry
</Button>
</Grid>
<Grid size={12}>
{entries.length > 0 && <Table columns={columns} data={entries} />}
</Grid>
</Grid>
<LogbookEntryForm
entryId={'1'}
isDrawerOpen={isDrawerOpen}
mode={entryFormMode}
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
/>
</Box>
); );
}; };

View File

@@ -0,0 +1,8 @@
import { FormMode } from '../../enums/formMode';
export interface ILogbookEntryFormProps {
entryId?: string;
isDrawerOpen: boolean;
mode: FormMode;
onOpenClose: (mode: FormMode) => void;
}

View File

@@ -1,122 +1,862 @@
// import React from 'react'; import React, { useEffect, useState } from 'react';
// import { import {
// Accordion, Accordion,
// AccordionItem, AccordionDetails,
// Button, AccordionSummary,
// DatePicker, Button,
// Input ChevronDownIcon,
// } from '@nextui-org/react'; DatePicker,
// import { useForm, Controller, SubmitHandler } from 'react-hook-form'; Drawer,
Grid,
IconButton,
SaveIcon,
Select,
TextField,
Typography,
XmarkIcon
} from '@noahspan/noahspan-components';
import {
useForm,
Controller,
SubmitHandler,
FormProvider
} from 'react-hook-form';
import { ILogbookEntryFormProps } from './ILogbookEntryFormProps';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
import { usePilots } from '../../hooks/pilots/UsePilots';
// const LogbookEntryForm: React.FC<unknown> = () => { const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
// const { control, handleSubmit } = useForm(); entryId,
isDrawerOpen,
mode,
onOpenClose
}) => {
const httpClient: AxiosInstance = useHttpClient();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [pilotOptions, setPilotOptions] = useState<
{ label: string; value: string }[]
>([]);
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
pilotId: '',
pilotName: '',
date: null,
aircraftMakeModel: '',
aircraftIdentity: '',
routeFrom: '',
routeTo: '',
durationOfFlight: null,
singleEngineLand: null,
simulatorAtd: null,
landingsDay: null,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: null,
night: null,
solo: null,
pilotInCommand: null,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: ''
};
const methods = useForm();
const { pilots } = usePilots();
// const onSubmit = (data: unknown) => { const onCancel = () => {
// console.log(data); methods.reset(defaultValues);
// }; onOpenClose(FormMode.CANCEL);
};
// return ( const onSubmit = async (data: unknown) => {
// <form className="m-10" onSubmit={handleSubmit(onSubmit)}> try {
// <div className="grid grid-cols-2 gap-4"> setIsLoading(true);
// <div className="self-center">
// <label>Date</label>
// </div>
// <div>
// <Controller
// name="date"
// control={control}
// render={({ field }) => <DatePicker labelPlacement="outside-left" />}
// />
// </div>
// <div className="self-center">
// <label>Aircraft Type</label>
// </div>
// <div>
// <Controller
// name="aircraftType"
// control={control}
// render={({ field }) => (
// <Input fullWidth={true} labelPlacement="outside-left" />
// )}
// />
// </div>
// <div className="self-center">
// <label>Aircraft Identity</label>
// </div>
// <div>
// <Controller
// name="aircraftIdent"
// control={control}
// render={({ field }) => (
// <Input fullWidth={true} labelPlacement="outside-left" />
// )}
// />
// </div>
// <div className="self-center">
// <label>Route To</label>
// </div>
// <div>
// <Controller
// name="routeTo"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className="self-center">
// <label>Route From</label>
// </div>
// <div>
// <Controller
// name="routeFrom"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className="self-center">
// <label>Duration of Flight</label>
// </div>
// <div>
// <Controller
// name="flightDuration"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className="self-center">
// <label>Single Engine Land</label>
// </div>
// <div>
// <Controller
// name="aircraftSEL"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// <div className="col-span-2">
// <Accordion>
// <AccordionItem title="Aircraft Category and Class">
// <div className="self-center">
// <label>Single Engine Land</label>
// </div>
// <div>
// <Controller
// name="aircraftSEL"
// control={control}
// render={({ field }) => <Input />}
// />
// </div>
// </AccordionItem>
// </Accordion>
// </div>
// <div className="col-span-2 justify-self-end">
// <Button color="default">Cancel</Button>
// <Button className="ml-10" color="primary">
// Save
// </Button>
// </div>
// </div>
// </form>
// );
// };
// export default LogbookEntryForm; const accessToken: string = await getAccessToken();
await httpClient.post(`api/logbook`, data, {
headers: {
Authorization: accessToken
}
});
methods.reset(defaultValues);
onOpenClose(FormMode.CANCEL);
} catch (error) {
if (axios.isAxiosError(error)) {
const errResp = error.response;
console.log(errResp?.data.message);
} else {
}
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (pilots) {
const newPilotsOptions = pilots.map((pilot) => {
return {
label: pilot.name,
value: pilot.id
};
});
setPilotOptions(newPilotsOptions);
}
}, [pilots]);
return (
<Drawer
open={isDrawerOpen}
anchor="right"
PaperProps={{
sx: {
padding: '30px',
width: '33%'
}
}}
>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Grid container spacing={2}>
<Grid size={11}>
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</Typography>
</Grid>
<Grid display="flex" justifyContent="right" size={1}>
<IconButton onClick={onCancel}>
<XmarkIcon />
</IconButton>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Pilot *</Typography>
</Grid>
<Grid size={8}>
<Controller
name="pilotId"
control={methods.control}
render={({ field: { onChange, value } }) => {
return (
<Select
fullWidth
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={
pilotOptions && pilotOptions.length > 0
? pilotOptions
: []
}
value={value}
/>
);
}}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Date *</Typography>
</Grid>
<Grid size={8}>
<Controller
name="date"
control={methods.control}
render={({ field: { onChange, value } }) => (
<DatePicker onChange={onChange} value={value} />
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Aircraft Make and Model *</Typography>
</Grid>
<Grid size={8}>
<Controller
name="aircraftMakeModel"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Aircraft Identity *</Typography>
</Grid>
<Grid size={8}>
<Controller
name="aircraftIdentity"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Route From</Typography>
</Grid>
<Grid size={8}>
<Controller
name="routeFrom"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Route To</Typography>
</Grid>
<Grid size={8}>
<Controller
name="routeTo"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Duration Of Flight</Typography>
</Grid>
<Grid size={8}>
<Controller
name="durationOfFlight"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Single Engine Land</Typography>
</Grid>
<Grid size={8}>
<Controller
name="singleEngineLand"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Simulator or ATD</Typography>
</Grid>
<Grid size={8}>
<Controller
name="simulatorAtd"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid size={12}>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ChevronDownIcon />}>
<Typography variant="body1">Landings</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Day</Typography>
</Grid>
<Grid size={8}>
<Controller
name="landingsDay"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Night</Typography>
</Grid>
<Grid size={8}>
<Controller
name="landingsNight"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
<Grid size={12}>
<Accordion>
<AccordionSummary expandIcon={<ChevronDownIcon />}>
Instrument
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Actual</Typography>
</Grid>
<Grid size={8}>
<Controller
name="instrumentActual"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Simulated</Typography>
</Grid>
<Grid size={8}>
<Controller
name="instrumentSimulated"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">
Instrument Approaches
</Typography>
</Grid>
<Grid size={8}>
<Controller
name="instrumentApproaches"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Holds</Typography>
</Grid>
<Grid size={8}>
<Controller
name="instrumentHolds"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Nav / Track</Typography>
</Grid>
<Grid size={8}>
<Controller
name="instrumentNavTrack"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
<Grid size={12}>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ChevronDownIcon />}>
Type of Pilot Experience or Training
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">
Ground Training Received
</Typography>
</Grid>
<Grid size={8}>
<Controller
name="groundTrainingReceived"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">
Flight Training Received
</Typography>
</Grid>
<Grid size={8}>
<Controller
name="flightTrainingReceived"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Cross Country</Typography>
</Grid>
<Grid size={8}>
<Controller
name="crossCountry"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Night</Typography>
</Grid>
<Grid size={8}>
<Controller
name="night"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Solo</Typography>
</Grid>
<Grid size={8}>
<Controller
name="solo"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Pilot in Command</Typography>
</Grid>
<Grid size={8}>
<Controller
name="pilotInCommand"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
<Grid size={4}>
<Typography variant="body1">Notes</Typography>
</Grid>
<Grid size={8}>
<Controller
name="notes"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
// disabled={isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
multiline
rows={3}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid display="flex" gap={2} justifyContent="right" size={12}>
<Button
// disabled={isDisabled}
startIcon={<XmarkIcon />}
variant="outlined"
onClick={onCancel}
data-testid="pilot-cancel-button"
size="small"
>
Cancel
</Button>
<Button
// disabled={isDisabled}
startIcon={<SaveIcon />}
size="small"
type="submit"
variant="contained"
data-testid="pilot-save-button"
>
Save
</Button>
</Grid>
</Grid>
</form>
</FormProvider>
</Drawer>
);
};
export default LogbookEntryForm;

View File

@@ -1,8 +1,8 @@
import { PilotFormMode } from './PilotForm'; import { FormMode } from '../../enums/formMode';
export interface IPilotFormProps { export interface IPilotFormProps {
isDrawerOpen: boolean; isDrawerOpen: boolean;
mode: PilotFormMode; mode: FormMode;
onOpenClose: (mode: PilotFormMode) => void; onOpenClose: (mode: FormMode) => void;
pilotId?: string; pilotId?: string;
} }

View File

@@ -3,13 +3,12 @@ import { useForm, Controller, FormProvider } from 'react-hook-form';
import { import {
Button, Button,
Drawer, Drawer,
DrawerBody, Grid,
DrawerHeader, IconButton,
DrawerFooter,
Input,
PeoplePicker, PeoplePicker,
SaveIcon, SaveIcon,
StateSelect, StateSelect,
TextField,
Typography, Typography,
XmarkIcon XmarkIcon
} from '@noahspan/noahspan-components'; } from '@noahspan/noahspan-components';
@@ -18,13 +17,8 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
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';
export enum PilotFormMode { import { Person } from '@microsoft/microsoft-graph-types';
ADD = 'ADD',
EDIT = 'EDIT',
VIEW = 'VIEW',
CANCEL = 'CANCEL'
}
const PilotForm: React.FC<IPilotFormProps> = ({ const PilotForm: React.FC<IPilotFormProps> = ({
pilotId, pilotId,
@@ -36,6 +30,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]); const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
const [isPeoplePickerLoading, setIsPeoplePickerLoading] = const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
useState<boolean>(false); useState<boolean>(false);
const [selectedPerson, setSelectedPerson] = useState<Person>({
userPrincipalName: '',
displayName: ''
});
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { getAccessToken } = useAccessToken(); const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
@@ -56,36 +54,29 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}); });
const [isDisabled, setIsDisabled] = useState<boolean>(false); const [isDisabled, setIsDisabled] = useState<boolean>(false);
const handlePeoplePickerOnClick = ( const onPeoplePickerSearch = async (
event: React.MouseEvent<HTMLDivElement> _event: React.SyntheticEvent,
) => { value: string
const divElement: HTMLDivElement = event.target as HTMLDivElement;
methods.setValue('id', divElement.id);
methods.setValue('name', divElement.textContent!);
setPeoplePickerResults([]);
};
const handlePeoplePickerOnChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => { ) => {
setIsPeoplePickerLoading(true); setIsPeoplePickerLoading(true);
try { try {
methods.setValue('name', event.target.value); if (value !== '') {
const searchString: string = value;
const searchString: string = event.target.value; const accessToken: string = await getAccessToken();
const accessToken: string = await getAccessToken(); const response: AxiosResponse = await httpClient.get(
const response: AxiosResponse = await httpClient.get( `api/personSearch?search=${searchString}`,
`api/personSearch?search=${searchString}`, {
{ headers: {
headers: { Authorization: accessToken
Authorization: accessToken }
} }
} );
);
console.log(response); setPeoplePickerResults(response.data);
setPeoplePickerResults(response.data); } else {
setPeoplePickerResults([]);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {
@@ -93,9 +84,20 @@ const PilotForm: React.FC<IPilotFormProps> = ({
} }
}; };
const onPeoplePickerSelectionChange = (
_event: React.SyntheticEvent,
value: Person,
_reason: string
) => {
methods.setValue('id', value.userPrincipalName!.toString());
methods.setValue('name', value.displayName!.toString());
setSelectedPerson(value);
};
const onCancel = () => { const onCancel = () => {
methods.reset(defaultValues); methods.reset(defaultValues);
onOpenClose(PilotFormMode.CANCEL); onOpenClose(FormMode.CANCEL);
setSelectedPerson({ userPrincipalName: '', displayName: '' });
}; };
const onSubmit = async (data: unknown) => { const onSubmit = async (data: unknown) => {
@@ -127,7 +129,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (mode === PilotFormMode.VIEW) { if (mode === FormMode.VIEW) {
setIsDisabled(true); setIsDisabled(true);
} }
}, [mode]); }, [mode]);
@@ -146,10 +148,14 @@ const PilotForm: React.FC<IPilotFormProps> = ({
); );
const pilot = response.data; const pilot = response.data;
console.log(pilot); console.log(pilot);
// methods.setValue('blah', pilot.value) setSelectedPerson({
userPrincipalName: pilot.id,
displayName: pilot.name
});
methods.reset(pilot); methods.reset(pilot);
console.log(methods.getValues()); console.log(methods.getValues());
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -163,251 +169,219 @@ const PilotForm: React.FC<IPilotFormProps> = ({
return ( return (
<Drawer <Drawer
open={isDrawerOpen} open={isDrawerOpen}
placement="right" anchor="right"
size={1000}
data-testid="pilot-drawer" data-testid="pilot-drawer"
PaperProps={{
sx: {
padding: '30px',
width: '33%'
}
}}
> >
<FormProvider {...methods}> <FormProvider {...methods}>
<DrawerHeader text="Add Pilot" onClose={onCancel} />
<form onSubmit={methods.handleSubmit(onSubmit)}> <form onSubmit={methods.handleSubmit(onSubmit)}>
<DrawerBody> <Grid container spacing={2}>
<div className="grid grid-cols-4 gap-4"> <Grid size={11}>
<div className="col-span-1"> <Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</Typography>
<Typography variant="h6">Name *</Typography> </Grid>
</div> <Grid display="flex" justifyContent="right" size={1}>
<div className="col-span-3"> <IconButton onClick={onCancel}>
<Controller <XmarkIcon />
name="name" </IconButton>
control={methods.control} </Grid>
rules={{ required: 'A name must be selected' }} <Grid size={3}>
render={({ field: { value } }) => ( <Typography variant="h6">Name *</Typography>
<PeoplePicker </Grid>
results={peoplePickerResults} <Grid size={9}>
inputProps={{ <PeoplePicker
disabled: isDisabled, disabled={isDisabled}
labelProps: { loading={isPeoplePickerLoading}
className: 'before:content-none after:content-none' onInputChanged={onPeoplePickerSearch}
}, onSelectionChanged={onPeoplePickerSelectionChange}
onChange: (event) => handlePeoplePickerOnChange(event), options={peoplePickerResults}
error: methods.formState.errors.name ? true : false, value={selectedPerson}
helperText: methods.formState.errors.name />
? methods.formState.errors.name.message?.toString() </Grid>
: undefined, <Grid size={3}>
value: value <Typography variant="h6">Address *</Typography>
}} </Grid>
listItemProps={{ <Grid size={9}>
children: null, <Controller
onClick: handlePeoplePickerOnClick name="address"
}} control={methods.control}
loading={isPeoplePickerLoading} rules={{ required: 'An address is required' }}
data-testid="pilot-form-people-picker" render={({ field: { onChange, value } }) => (
/> <TextField
)} disabled={isDisabled}
/> error={methods.formState.errors.address ? true : false}
</div> fullWidth
{isAuthenticated && ( helperText={
<> methods.formState.errors.address
<div className="col-span-1"> ? methods.formState.errors.address.message
<Typography variant="h6">Address *</Typography> : undefined
</div> }
<div className="col-span-3"> onChange={onChange}
<Controller value={value}
name="address" />
control={methods.control} )}
rules={{ required: 'An address is required' }} />
render={({ field: { onChange, value } }) => ( </Grid>
<Input <Grid size={3}>
className="!border-t-blue-gray-200 focus:!border-t-gray-900" <Typography variant="h6">City *</Typography>
disabled={isDisabled} </Grid>
labelProps={{ <Grid size={9}>
className: 'before:content-none after:content-none' <Controller
}} name="city"
error={ control={methods.control}
methods.formState.errors.address ? true : false rules={{ required: 'A city is required' }}
} render={({ field: { onChange, value } }) => (
helperText={ <TextField
methods.formState.errors.address disabled={isDisabled}
? methods.formState.errors.address.message?.toString() error={methods.formState.errors.city ? true : false}
: undefined fullWidth
} helperText={
onChange={onChange} methods.formState.errors.city
value={value} ? methods.formState.errors.city.message
data-testid="pilot-form-address-input" : undefined
/> }
)} onChange={onChange}
/> value={value}
</div> />
</> )}
)} />
{isAuthenticated && ( </Grid>
<> <Grid size={3}>
<div className="col-span-1"> <Typography variant="h6">State *</Typography>
<Typography variant="h6">City *</Typography> </Grid>
</div> <Grid size={9}>
<div className="col-span-3"> <Controller
<Controller name="state"
name="city" control={methods.control}
control={methods.control} rules={{ required: 'A state must be selected' }}
rules={{ required: 'A city is required' }} render={({ field: { onChange, value } }) => (
render={({ field: { onChange, value } }) => ( <StateSelect
<Input disabled={isDisabled}
disabled={isDisabled} // error={methods.formState.errors.state ? true : false}
labelProps={{ fullWidth
className: 'before:content-none after:content-none' // helperText={
}} // methods.formState.errors.state
error={methods.formState.errors.city ? true : false} // ? methods.formState.errors.state.message?.toString()
helperText={ // : undefined
methods.formState.errors.city // }
? methods.formState.errors.city.message?.toString() onChange={onChange}
: undefined value={value}
} variant="outlined"
onChange={onChange} data-testid="pilot-form-state-dropdown"
value={value} />
data-testid="pilot-form-city-input" )}
/> />
)} </Grid>
/> <Grid size={3}>
</div> <Typography variant="h6">Postal Code *</Typography>
</> </Grid>
)} <Grid size={9}>
{isAuthenticated && ( <Controller
<> name="postalCode"
<div className="col-span-1"> control={methods.control}
<Typography variant="h6">State *</Typography> rules={{ required: 'A postal code is required' }}
</div> render={({ field: { onChange, value } }) => (
<div className="col-span-3"> <TextField
<Controller disabled={isDisabled}
name="state" error={methods.formState.errors.postalCode ? true : false}
control={methods.control} fullWidth
rules={{ required: 'A state must be selected' }} helperText={
render={({ field: { onChange, value } }) => ( methods.formState.errors.postalCode
<StateSelect ? methods.formState.errors.postalCode.message
disabled={isDisabled} : undefined
labelProps={{ }
className: 'before:content-none after:content-none' onChange={onChange}
}} value={value}
error={methods.formState.errors.state ? true : false} />
helperText={ )}
methods.formState.errors.state />
? methods.formState.errors.state.message?.toString() </Grid>
: undefined <Grid size={3}>
} <Typography variant="h6">Email</Typography>
onChange={onChange} </Grid>
value={value} <Grid size={9}>
variant="outlined" <Controller
data-testid="pilot-form-state-dropdown" name="email"
/> control={methods.control}
)} rules={{
/> pattern: {
</div> value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
</> message: 'Invalid email address'
)} }
{isAuthenticated && ( }}
<> render={({ field: { onChange, value } }) => (
<div className="col-span-1"> <TextField
<Typography variant="h6">Postal Code *</Typography> disabled={isDisabled}
</div> fullWidth
<div className="col-span-3"> error={methods.formState.errors.email ? true : false}
<Controller helperText={
name="postalCode" methods.formState.errors.email
control={methods.control} ? methods.formState.errors.email.message
rules={{ required: 'A postal code is required' }} : undefined
render={({ field: { onChange, value } }) => ( }
<Input onChange={onChange}
disabled={isDisabled} value={value}
labelProps={{ />
className: 'before:content-none after:content-none' )}
}} />
error={ </Grid>
methods.formState.errors.postalCode ? true : false <Grid size={3}>
} <Typography variant="h6">Phone Number</Typography>
helperText={ </Grid>
methods.formState.errors.postalCode <Grid size={9}>
? methods.formState.errors.postalCode.message?.toString() <Controller
: undefined name="phone"
} control={methods.control}
onChange={onChange} rules={{
value={value} pattern: {
data-testid="pilot-form-postal-code-input" value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
/> message: 'Enter phone number as 123-456-7890'
)} }
/> }}
</div> render={({ field: { onChange, value } }) => (
</> <TextField
)} disabled={isDisabled}
{isAuthenticated && ( fullWidth
<> error={methods.formState.errors.phone ? true : false}
<div className="col-span-1"> helperText={
<Typography variant="h6">Email</Typography> methods.formState.errors.phone
</div> ? methods.formState.errors.phone.message
<div className="col-span-3"> : undefined
<Controller }
name="email" onChange={onChange}
control={methods.control} value={value}
rules={{ />
pattern: { )}
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, />
message: 'Invalid email address' </Grid>
} <Grid display="flex" gap={2} justifyContent="right" size={12}>
}} <Button
render={({ field: { onChange, value } }) => ( disabled={isDisabled}
<Input startIcon={<XmarkIcon />}
disabled={isDisabled} variant="outlined"
labelProps={{ onClick={onCancel}
className: 'before:content-none after:content-none' data-testid="pilot-cancel-button"
}} size="small"
error={methods.formState.errors.email ? true : false} >
helperText={ Cancel
methods.formState.errors.email </Button>
? methods.formState.errors.email.message?.toString() <Button
: undefined disabled={isDisabled}
} startIcon={<SaveIcon />}
onChange={onChange} size="small"
value={value} type="submit"
data-testid="pilot-form-email-input" variant="contained"
/> data-testid="pilot-save-button"
)} >
/> Save
</div> </Button>
</> </Grid>
)} </Grid>
{isAuthenticated && ( {/* {pilotId && (
<>
<div className="col-span-1">
<Typography variant="h6">Phone Number</Typography>
</div>
<div className="col-span-3">
<Controller
name="phone"
control={methods.control}
rules={{
pattern: {
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
message: 'Enter phone number as 123-456-7890'
}
}}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.phone ? true : false}
helperText={
methods.formState.errors.phone
? methods.formState.errors.phone.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-phone-input"
/>
)}
/>
</div>
</>
)}
{/* {pilotId && (
<> <>
<div className="col-span-1"> <div className="col-span-1">
<Typography variant="h6">Last Review</Typography> <Typography variant="h6">Last Review</Typography>
@@ -432,7 +406,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
</div> </div>
</> </>
)} */} )} */}
{/* {pilotId && ( {/* {pilotId && (
<> <>
<div className="col-span-4"> <div className="col-span-4">
<Typography variant="h5">Medical</Typography> <Typography variant="h5">Medical</Typography>
@@ -512,41 +486,34 @@ const PilotForm: React.FC<IPilotFormProps> = ({
<PilotFormEndorsements endorsements={[]} /> <PilotFormEndorsements endorsements={[]} />
</> </>
)} */} )} */}
</div> {/* </div>
</DrawerBody> {mode !== FormMode.VIEW && (
<DrawerFooter>
<>
{mode !== PilotFormMode.VIEW && (
<div className="flex gap-2 justify-end justify-self-center pt-4"> <div className="flex gap-2 justify-end justify-self-center pt-4">
<div> <div>
<Button <Button
className="flex items-center gap-3"
disabled={isDisabled} disabled={isDisabled}
variant="outlined" startIcon={<XmarkIcon />}
variant='contained'
onClick={onCancel} onClick={onCancel}
data-testid="pilot-cancel-button" data-testid="pilot-cancel-button"
> >
<XmarkIcon size="lg" />
Cancel Cancel
</Button> </Button>
</div> </div>
<div> <div>
<Button <Button
className="flex items-center gap-3"
disabled={isDisabled} disabled={isDisabled}
loading={isLoading} startIcon={<SaveIcon />}
variant="filled" size='medium'
type="submit" type="submit"
variant='contained'
data-testid="pilot-save-button" data-testid="pilot-save-button"
> >
<SaveIcon size="lg" />
Save Save
</Button> </Button>
</div> </div>
</div> </div>
)} )} */}
</>
</DrawerFooter>
</form> </form>
</FormProvider> </FormProvider>
</Drawer> </Drawer>

View File

@@ -1,18 +1,20 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import PilotForm from '../pilotForm/PilotForm'; import PilotForm from '../pilotForm/PilotForm';
import { import {
Box,
Button, Button,
ColumnDef,
EllipsisVerticalIcon, EllipsisVerticalIcon,
EyeIcon, EyeIcon,
Grid,
IconButton, IconButton,
ListItemIcon,
ListItemText,
Menu, Menu,
MenuHandler,
MenuItem, MenuItem,
MenuList,
PenIcon, PenIcon,
PlusIcon, PlusIcon,
Table, Table,
TableColumnDef,
TrashIcon, TrashIcon,
Typography Typography
} from '@noahspan/noahspan-components'; } from '@noahspan/noahspan-components';
@@ -20,28 +22,33 @@ import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios'; import { 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 { PilotFormMode } from '../pilotForm/PilotForm'; import { FormMode } from '../../enums/formMode';
type Pilot = {
partitionKey: string;
rowKey: string;
id: string;
name: string;
};
const Pilots: React.FC<unknown> = () => { const Pilots: React.FC<unknown> = () => {
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 [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [pilotFormMode, setPilotFormMode] = useState<PilotFormMode>( const [pilotFormMode, setPilotFormMode] = useState<FormMode>(FormMode.CANCEL);
PilotFormMode.CANCEL
);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>(); const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]); const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenClosePilotForm = (mode: PilotFormMode, pilotId?: string) => { const onOpenClosePilotForm = (mode: FormMode, pilotId?: string) => {
switch (mode) { switch (mode) {
case PilotFormMode.ADD: case FormMode.ADD:
case PilotFormMode.EDIT: case FormMode.EDIT:
case PilotFormMode.VIEW: case FormMode.VIEW:
setPilotFormMode(mode); setPilotFormMode(mode);
setSelectedPilotId(pilotId); setSelectedPilotId(pilotId);
setIsDrawerOpen(true); setIsDrawerOpen(true);
break; break;
case PilotFormMode.CANCEL: case FormMode.CANCEL:
setPilotFormMode(mode); setPilotFormMode(mode);
setSelectedPilotId(undefined); setSelectedPilotId(undefined);
setIsDrawerOpen(false); setIsDrawerOpen(false);
@@ -49,64 +56,70 @@ const Pilots: React.FC<unknown> = () => {
} }
}; };
type Pilot = { interface ActionMenuProps {
partitionKey: string; pilotId: string;
rowKey: string; }
id: string;
name: string; const ActionMenu = ({ pilotId }: ActionMenuProps) => {
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
null
);
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
console.log(event);
setAnchorElAction(event.currentTarget);
};
const onCloseActionMenu = () => {
setAnchorElAction(null);
};
return (
<div>
<IconButton onClick={onOpenActionMenu}>
<EllipsisVerticalIcon size="sm" />
</IconButton>
<Menu
anchorEl={anchorElAction}
keepMounted
open={Boolean(anchorElAction)}
onClose={onCloseActionMenu}
>
<MenuItem
onClick={() => onOpenClosePilotForm(FormMode.EDIT, pilotId)}
>
<ListItemIcon>
<PenIcon size="lg" />
</ListItemIcon>
<ListItemText>Edit</ListItemText>
</MenuItem>
<MenuItem
onClick={() => onOpenClosePilotForm(FormMode.VIEW, pilotId)}
>
<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: TableColumnDef[] = [ const columns: ColumnDef<Pilot>[] = [
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'Name' header: 'Name'
}, },
{ {
id: 'actions',
header: 'Actions', header: 'Actions',
cellProps: { cell: (info) => <ActionMenu pilotId={info.row.original.rowKey} />
className: 'text-end'
},
cell: (info: any) => {
const pilotId = info.row.original.rowKey;
return (
<Menu placement="bottom-end">
<MenuHandler>
<div>
<IconButton variant="text">
<EllipsisVerticalIcon size="xl" />
</IconButton>
</div>
</MenuHandler>
<MenuList>
<MenuItem
className="flex gap-3"
onClick={() =>
onOpenClosePilotForm(PilotFormMode.EDIT, pilotId)
}
>
<PenIcon size="lg" />
Edit
</MenuItem>
<MenuItem
className="flex gap-3"
onClick={() =>
onOpenClosePilotForm(PilotFormMode.VIEW, pilotId)
}
>
<EyeIcon size="lg" />
View
</MenuItem>
<hr className="my-3" />
<MenuItem className="flex gap-3">
<TrashIcon size="lg" />
Delete
</MenuItem>
</MenuList>
</Menu>
);
},
enableSorting: false
} }
]; ];
@@ -131,31 +144,32 @@ const Pilots: React.FC<unknown> = () => {
}, []); }, []);
return ( return (
<> <Box sx={{ margin: '20px' }}>
<div className="grid grid-cols-1 gap-4 w-full rounded-xl py-4 px-8 shadow-md backdrop-saturate-200 backdrop-blur-2xl bg-opacity-80 border border-white/80 bg-white mt-6"> <Grid container spacing={2}>
<div className="col-span-1"> <Grid size={11}>
<Typography variant="h2">Pilots</Typography> <Typography variant="h4">Pilots</Typography>
</div> </Grid>
<div className="col-span-1 justify-self-end"> <Grid display="flex" justifyContent="right" size={1}>
<Button <Button
className="flex justify-center gap-3" onClick={() => onOpenClosePilotForm(FormMode.ADD)}
variant="filled" startIcon={<PlusIcon />}
onClick={() => onOpenClosePilotForm(PilotFormMode.ADD)} variant="contained"
data-testid="pilot-add-button" data-testid="pilot-add-button"
> >
<PlusIcon size="lg" />
Add Pilot Add Pilot
</Button> </Button>
</div> </Grid>
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />} <Grid size={12}>
</div> {pilots.length > 0 && <Table columns={columns} data={pilots} />}
</Grid>
</Grid>
<PilotForm <PilotForm
isDrawerOpen={isDrawerOpen} isDrawerOpen={isDrawerOpen}
mode={pilotFormMode} mode={pilotFormMode}
onOpenClose={(mode) => onOpenClosePilotForm(mode)} onOpenClose={(mode) => onOpenClosePilotForm(mode)}
pilotId={selectedPilotId} pilotId={selectedPilotId}
/> />
</> </Box>
); );
}; };

View File

@@ -1,19 +1,14 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ISiteNavProps } from './ISiteNavProps'; import { ISiteNavProps } from './ISiteNavProps';
// import { Link as ReactRouterLink } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAppContext } from '../../hooks/appContext/UseAppContext'; import { useAppContext } from '../../hooks/appContext/UseAppContext';
import { import {
Avatar, Avatar,
Button, Button,
IconButton,
Menu, Menu,
MenuHandler,
MenuItem, MenuItem,
MenuList,
Navbar, Navbar,
NavbarBrand,
NavbarLinks,
NavbarMenu,
NavbarItemProps,
PlaneIcon, PlaneIcon,
SignOutIcon, SignOutIcon,
Spinner, Spinner,
@@ -37,10 +32,15 @@ const SiteNav: React.FC<unknown> = () => {
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken(); const { getAccessToken } = useAccessToken();
const { inProgress, instance } = useMsal(); const { inProgress, instance } = useMsal();
const navItems: NavbarItemProps[] = [ const navigate = useNavigate();
const pages = [
{
name: 'Logbook',
url: '/'
},
{ {
name: 'Pilots', name: 'Pilots',
url: '#' url: '/pilots'
} }
]; ];
const handleSignIn = async () => { const handleSignIn = async () => {
@@ -83,6 +83,20 @@ const SiteNav: React.FC<unknown> = () => {
throw new Error(); throw new Error();
} }
}; };
const handlePageClick = (url: string) => {
navigate(url);
};
const Settings = () => {
return (
<MenuItem onClick={handleSignOut}>
<SignOutIcon />
<Typography sx={{ marginLeft: '10px', textAlign: 'center' }}>
Sign Out
</Typography>
</MenuItem>
);
};
useEffect(() => { useEffect(() => {
const callback = instance.addEventCallback( const callback = instance.addEventCallback(
@@ -150,107 +164,16 @@ const SiteNav: React.FC<unknown> = () => {
}, [isAuthenticated]); }, [isAuthenticated]);
return ( return (
<Navbar className="mt-6"> <Navbar
<NavbarBrand> handlePageClick={handlePageClick}
<img height={40} width={40} src="noahspan-logo.png" />{' '} handleSignIn={handleSignIn}
<PlaneIcon size="2x" /> isAuthenticated={isAuthenticated}
</NavbarBrand> logo={<PlaneIcon size="2x" />}
<NavbarLinks items={navItems} /> pages={pages}
<NavbarMenu> settings={<Settings />}
<div className="flex items-center gap-2 hidden lg:inline-block"> userPhoto={userPhoto}
{!loading && isAuthenticated && ( />
<Menu placement="bottom-end">
<MenuHandler>
<div>
{userPhoto && (
<img
className="rounded-full cursor-pointer"
height="40"
width="40"
src={userPhoto}
/>
)}
{!userPhoto && (
<div className="rounded-full cursor-pointer text-white text-center pt-2 bg-black h-[40px] w-[40px]">
NS
</div>
)}
</div>
</MenuHandler>
<MenuList>
<MenuItem onClick={handleSignOut}>
<Typography
className="flex justify-center gap-3"
variant="small"
>
<SignOutIcon size="lg" />
Sign Out
</Typography>
</MenuItem>
</MenuList>
</Menu>
)}
{loading && isAuthenticated && (
<div className="flex justify-center gap-3">
<Spinner size="xs" />
<Typography variant="small">Loading...</Typography>
</div>
)}
{!isAuthenticated && (
<Button
variant="text"
size="sm"
onClick={handleSignIn}
loading={inProgress === InteractionStatus.Login ? true : false}
>
Sign In
</Button>
)}
</div>
{/* <IconButton
variant='text'
className='ml-auto h-6 w-6 text-inherit hover:bg-transparent focus:bg-transparent active:bg-transparent lg:hidden'
ripple={false}
onClick={() => setOpenNav(!openNav)}
>
<FontAwesomeIcon icon={faBars} size='2x' />
</IconButton> */}
</NavbarMenu>
</Navbar>
); );
// return (
// <Navbar
// classNames={{
// base: 'bg-transparent z-0'
// }}
// isBlurred={false}
// maxWidth="full"
// data-testid="flying-navbar"
// >
// <NavbarBrand>
// <Logo className="pr-3" height={50} width={50} data-testid="logo" />
// <Plane size="2xl" />
// </NavbarBrand>
// <NavbarContent justify="center">
// <NavbarItem>
// {appContext.state.featureFlags.find(
// (featureFlag) => featureFlag.key === 'flying-pilots'
// )?.enabled && (
// <Link>
// <ReactRouterLink to="/">Pilots</ReactRouterLink>
// </Link>
// )}
// </NavbarItem>
// </NavbarContent>
// <NavbarContent justify='end'>
// <Login
// loginCompleted={loginCompleted}
// loginView='compact'
// />
// </NavbarContent>
// </Navbar>
// );
}; };
export default SiteNav; export default SiteNav;

View File

@@ -0,0 +1,6 @@
export enum FormMode {
ADD = 'ADD',
EDIT = 'EDIT',
VIEW = 'VIEW',
CANCEL = 'CANCEL'
}

View File

@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
export const usePilots = () => {
const [pilots, setPilots] = useState<any[]>();
const httpClient: AxiosInstance = useHttpClient();
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const getPilot = async (pilotId: string) => {
try {
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots/${pilotId}`
);
return response.data;
} catch (error) {
return error;
}
};
useEffect(() => {
const getPilots = async () => {
try {
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots`,
config
);
setPilots(response.data);
} catch (error) {
return error;
}
};
getPilots();
}, []);
return {
getPilot,
pilots
};
};

View File

@@ -4,7 +4,7 @@
@layer base { @layer base {
body { body {
@apply bg-[#ECEFF1]; @apply bg-[#fafaf9];
@apply text-black; @apply text-black;
} }
} }

View File

@@ -8,7 +8,7 @@ import '@noahspan/noahspan-components/noahspan-components.css';
import { PublicClientApplication } from '@azure/msal-browser'; import { PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react'; import { MsalProvider } from '@azure/msal-react';
const pca: PublicClientApplication = new PublicClientApplication({ const msalInstance: PublicClientApplication = new PublicClientApplication({
auth: { auth: {
clientId: import.meta.env.VITE_CLIENT_ID, clientId: import.meta.env.VITE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
@@ -16,14 +16,16 @@ const pca: PublicClientApplication = new PublicClientApplication({
} }
}); });
ReactDOM.createRoot(document.getElementById('root')!).render( msalInstance.initialize().then(() => {
<React.StrictMode> ReactDOM.createRoot(document.getElementById('root')!).render(
<MsalProvider instance={pca}> <React.StrictMode>
<AppContextProvider> <MsalProvider instance={msalInstance}>
<BrowserRouter> <AppContextProvider>
<App /> <BrowserRouter>
</BrowserRouter> <App />
</AppContextProvider> </BrowserRouter>
</MsalProvider> </AppContextProvider>
</React.StrictMode> </MsalProvider>
); </React.StrictMode>
);
});

27
compose.yaml Normal file
View File

@@ -0,0 +1,27 @@
version: '3.8'
services:
azurite:
container_name: azurite
image: mcr.microsoft.com/azure-storage/azurite
ports:
- '10000:10000'
- '10001:10001'
- '10002:10002'
command: 'azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --loose'
api:
container_name: api
build:
context: ./api
dockerfile: Dockerfile
ports:
- '7071:3000'
env_file:
- ./api/.env
# swa:
# image: swacli/static-web-apps-cli
# ports:
# - "4280:4280"
# command: "swa start "

4142
package-lock.json generated

File diff suppressed because it is too large Load Diff