adding new logbook entry (#24)

* adding new logbook entry

* adding new logbook entry
This commit was merged in pull request #24.
This commit is contained in:
2024-11-17 13:56:01 +00:00
committed by GitHub
parent 11e00b5b14
commit 52b29163e6
29 changed files with 6206 additions and 1070 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/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@noahspan/noahspan-modules": "^0.4.0",
"@noahspan/noahspan-modules": "^0.4.7",
"@schematics/angular": "^17.3.7",
"dotenv": "^16.4.5",
"reflect-metadata": "0.1.13",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@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';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { LogbookModule } from './logbook/logbook.module';
import { PilotModule } from './pilot/pilot.module';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter';
@@ -32,6 +33,7 @@ import { HttpExceptionFilter } from './filters/http-exception.filter';
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET
}),
LogbookModule,
PilotModule
],
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,6 +82,7 @@ export class PilotInfoService {
}
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
try {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
const pilotInfo: PilotInfoEntity = new PilotInfoEntity();
@@ -89,12 +90,11 @@ export class PilotInfoService {
Object.assign(pilotInfo, pilotInfoData);
pilotInfo.partitionKey = 'pilot';
pilotInfo.rowKey = pilotInfo.id;
try {
console.log(pilotInfo);
return await client.createEntity(pilotInfo);
} catch (error) {
const restError: RestError = error as RestError;
console.log(restError);
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],

View File

@@ -7,7 +7,11 @@ import { PilotInfoService } from './info/pilot-info.service';
imports: [
TableModule.register({
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],

View File

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

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import Pilots from './components/pilots/Pilots';
import Logbook from './components/logbook/Logbook';
import { useAppContext } from './hooks/appContext/UseAppContext';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
@@ -35,12 +36,13 @@ const App: React.FC<unknown> = () => {
}, []);
return (
<div className="container mx-auto">
<div>
<SiteNav />
<Routes>
{useFeatureFlag('flying-pilots')?.enabled && (
<Route path="/" element={<Pilots />} />
<Route path="/pilots" element={<Pilots />} />
)}
<Route path="/" element={<Logbook />} />
</Routes>
</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 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 (
<div>
<div>Logbook goes here</div>
</div>
<Box sx={{ margin: '20px' }}>
<Grid container spacing={2}>
<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 {
// Accordion,
// AccordionItem,
// Button,
// DatePicker,
// Input
// } from '@nextui-org/react';
// import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import React, { useEffect, useState } from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ChevronDownIcon,
DatePicker,
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 { control, handleSubmit } = useForm();
const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
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) => {
// console.log(data);
// };
const onCancel = () => {
methods.reset(defaultValues);
onOpenClose(FormMode.CANCEL);
};
// return (
// <form className="m-10" onSubmit={handleSubmit(onSubmit)}>
// <div className="grid grid-cols-2 gap-4">
// <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>
// );
// };
const onSubmit = async (data: unknown) => {
try {
setIsLoading(true);
// 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 {
isDrawerOpen: boolean;
mode: PilotFormMode;
onOpenClose: (mode: PilotFormMode) => void;
mode: FormMode;
onOpenClose: (mode: FormMode) => void;
pilotId?: string;
}

View File

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

View File

@@ -1,134 +1,134 @@
import { IPilotFormCertificates } from './IPilotFormCertificates';
import {
Button,
DatePicker,
Input,
Option,
PlusIcon,
Select,
TrashIcon,
Typography
} from '@noahspan/noahspan-components';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
// import { IPilotFormCertificates } from './IPilotFormCertificates';
// import {
// Button,
// DatePicker,
// Input,
// Option,
// PlusIcon,
// Select,
// TrashIcon,
// Typography
// } from '@noahspan/noahspan-components';
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
certificates
}: IPilotFormCertificates) => {
const {
control,
formState: { errors },
setValue
} = useFormContext();
// const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
// certificates
// }: IPilotFormCertificates) => {
// const {
// control,
// formState: { errors },
// setValue
// } = useFormContext();
const { fields, append, remove } = useFieldArray({
name: 'certificates',
control
});
// const { fields, append, remove } = useFieldArray({
// name: 'certificates',
// control
// });
return (
<>
{fields.length > 0 && (
<>
<div className="col-span-1">
<Typography variant="h6">Type</Typography>
</div>
<div className="col-span-1">
<Typography variant="h6">Number</Typography>
</div>
<div className="col-span-1">
<Typography variant="h6">Date of Issue</Typography>
</div>
<div className="col-span-1"></div>
</>
)}
{fields.map((field, index) => {
return (
<>
<div className="col-span-1">
<Controller
name={`certificates.${index}.type`}
control={control}
render={({ field }) => {
return (
<Select label="Type" {...field}>
<Option key="student" value="Student">
Student
</Option>
<Option key="private" value="Private">
Private
</Option>
<Option key="instrument" value="Instrument">
Instrument
</Option>
<Option key="recreational" value="Recreational">
Recreational
</Option>
<Option key="sport" value="Sport">
Sport
</Option>
</Select>
);
}}
/>
</div>
<div className="col-span-1">
<Controller
name={`certificates.${index}.number`}
control={control}
render={({ field }) => {
return <Input label="Number" {...field} />;
}}
/>
</div>
<div className="col-span-1">
<Controller
name={`certificates.${index}.dateOfIssue`}
control={control}
render={({ field }) => {
return (
<DatePicker
handleDateChanged={(date: string) => {
setValue(`certificates.${index}.dateOfIssue`, date);
}}
inputProps={{
value: field.value
}}
/>
);
}}
/>
</div>
<div className="col-span-1">
<Button
className="flex items-center gap-3"
onClick={() => remove(index)}
variant="outlined"
>
<TrashIcon size="lg" />
Delete
</Button>
</div>
</>
);
})}
<div className="col-span-4">
<Button
className="flex items-center gap-3"
onClick={() => {
append({
type: '',
number: '',
dateOfIssue: null
});
}}
variant="outlined"
>
<PlusIcon size="lg" />
Add Certificate
</Button>
</div>
</>
);
};
// return (
// <>
// {fields.length > 0 && (
// <>
// <div className="col-span-1">
// <Typography variant="h6">Type</Typography>
// </div>
// <div className="col-span-1">
// <Typography variant="h6">Number</Typography>
// </div>
// <div className="col-span-1">
// <Typography variant="h6">Date of Issue</Typography>
// </div>
// <div className="col-span-1"></div>
// </>
// )}
// {fields.map((field, index) => {
// return (
// <>
// <div className="col-span-1">
// <Controller
// name={`certificates.${index}.type`}
// control={control}
// render={({ field }) => {
// return (
// <Select label="Type" {...field}>
// <Option key="student" value="Student">
// Student
// </Option>
// <Option key="private" value="Private">
// Private
// </Option>
// <Option key="instrument" value="Instrument">
// Instrument
// </Option>
// <Option key="recreational" value="Recreational">
// Recreational
// </Option>
// <Option key="sport" value="Sport">
// Sport
// </Option>
// </Select>
// );
// }}
// />
// </div>
// <div className="col-span-1">
// <Controller
// name={`certificates.${index}.number`}
// control={control}
// render={({ field }) => {
// return <Input label="Number" {...field} />;
// }}
// />
// </div>
// <div className="col-span-1">
// <Controller
// name={`certificates.${index}.dateOfIssue`}
// control={control}
// render={({ field }) => {
// return (
// <DatePicker
// handleDateChanged={(date: string) => {
// setValue(`certificates.${index}.dateOfIssue`, date);
// }}
// inputProps={{
// value: field.value
// }}
// />
// );
// }}
// />
// </div>
// <div className="col-span-1">
// <Button
// className="flex items-center gap-3"
// onClick={() => remove(index)}
// variant="outlined"
// >
// <TrashIcon size="lg" />
// Delete
// </Button>
// </div>
// </>
// );
// })}
// <div className="col-span-4">
// <Button
// className="flex items-center gap-3"
// onClick={() => {
// append({
// type: '',
// number: '',
// dateOfIssue: null
// });
// }}
// variant="outlined"
// >
// <PlusIcon size="lg" />
// Add Certificate
// </Button>
// </div>
// </>
// );
// };
export default PilotFormCertificates;
// export default PilotFormCertificates;

View File

@@ -1,120 +1,120 @@
import { IPilotFormEndorsements } from './IPilotFormEndorsements';
import {
Button,
DatePicker,
Input,
Option,
PlusIcon,
Select,
TrashIcon,
Typography
} from '@noahspan/noahspan-components';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
// import { IPilotFormEndorsements } from './IPilotFormEndorsements';
// import {
// Button,
// DatePicker,
// Input,
// Option,
// PlusIcon,
// Select,
// TrashIcon,
// Typography
// } from '@noahspan/noahspan-components';
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
endorsements
}: IPilotFormEndorsements) => {
const {
control,
formState: { errors },
setValue
} = useFormContext();
// const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
// endorsements
// }: IPilotFormEndorsements) => {
// const {
// control,
// formState: { errors },
// setValue
// } = useFormContext();
const { fields, append, remove } = useFieldArray({
name: 'endorsements',
control
});
// const { fields, append, remove } = useFieldArray({
// name: 'endorsements',
// control
// });
return (
<>
{fields.length > 0 && (
<>
<div className="col-span-1">
<Typography variant="h6">Type</Typography>
</div>
<div className="col-span-1">
<Typography variant="h6">Date of Issue</Typography>
</div>
<div className="col-span-1"></div>
<div className="col-span-1"></div>
</>
)}
{fields.map((field, index) => {
return (
<>
<div className="col-span-1">
<Controller
name={`endorsements.${index}.type`}
control={control}
render={({ field }) => {
return (
<Select label="Type" {...field}>
<Option key="complex" value="Complex">
Complex
</Option>
<Option key="highPerformance" value="High Performance">
High Performance
</Option>
<Option key="highAltitude" value="High Altitude">
High Altitude
</Option>
<Option key="tailwheel" value="Tailwheel">
Tailwheel
</Option>
</Select>
);
}}
/>
</div>
<div className="col-span-1">
<Controller
name={`endorsements.${index}.dateOfIssue`}
control={control}
render={({ field }) => {
return (
<DatePicker
handleDateChanged={(date: string) => {
setValue(`endorsements.${index}.dateOfIssue`, date);
}}
inputProps={{
value: field.value
}}
/>
);
}}
/>
</div>
<div className="col-span-1">
<Button
className="flex items-center gap-3"
onClick={() => remove(index)}
variant="outlined"
>
<TrashIcon size="lg" />
Delete
</Button>
</div>
</>
);
})}
<div className="col-span-4">
<Button
className="flex items-center gap-3"
onClick={() => {
append({
type: '',
number: '',
dateOfIssue: null
});
}}
variant="outlined"
>
<PlusIcon size="lg" />
Add Endorsement
</Button>
</div>
</>
);
};
// return (
// <>
// {fields.length > 0 && (
// <>
// <div className="col-span-1">
// <Typography variant="h6">Type</Typography>
// </div>
// <div className="col-span-1">
// <Typography variant="h6">Date of Issue</Typography>
// </div>
// <div className="col-span-1"></div>
// <div className="col-span-1"></div>
// </>
// )}
// {fields.map((field, index) => {
// return (
// <>
// <div className="col-span-1">
// <Controller
// name={`endorsements.${index}.type`}
// control={control}
// render={({ field }) => {
// return (
// <Select label="Type" {...field}>
// <Option key="complex" value="Complex">
// Complex
// </Option>
// <Option key="highPerformance" value="High Performance">
// High Performance
// </Option>
// <Option key="highAltitude" value="High Altitude">
// High Altitude
// </Option>
// <Option key="tailwheel" value="Tailwheel">
// Tailwheel
// </Option>
// </Select>
// );
// }}
// />
// </div>
// <div className="col-span-1">
// <Controller
// name={`endorsements.${index}.dateOfIssue`}
// control={control}
// render={({ field }) => {
// return (
// <DatePicker
// handleDateChanged={(date: string) => {
// setValue(`endorsements.${index}.dateOfIssue`, date);
// }}
// inputProps={{
// value: field.value
// }}
// />
// );
// }}
// />
// </div>
// <div className="col-span-1">
// <Button
// className="flex items-center gap-3"
// onClick={() => remove(index)}
// variant="outlined"
// >
// <TrashIcon size="lg" />
// Delete
// </Button>
// </div>
// </>
// );
// })}
// <div className="col-span-4">
// <Button
// className="flex items-center gap-3"
// onClick={() => {
// append({
// type: '',
// number: '',
// dateOfIssue: null
// });
// }}
// variant="outlined"
// >
// <PlusIcon size="lg" />
// Add Endorsement
// </Button>
// </div>
// </>
// );
// };
export default PilotFormEndorsements;
// export default PilotFormEndorsements;

View File

@@ -1,18 +1,20 @@
import { useEffect, useState } from 'react';
import PilotForm from '../pilotForm/PilotForm';
import {
Box,
Button,
ColumnDef,
EllipsisVerticalIcon,
EyeIcon,
Grid,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuHandler,
MenuItem,
MenuList,
PenIcon,
PlusIcon,
Table,
TableColumnDef,
TrashIcon,
Typography
} from '@noahspan/noahspan-components';
@@ -20,28 +22,33 @@ import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
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 httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [pilotFormMode, setPilotFormMode] = useState<PilotFormMode>(
PilotFormMode.CANCEL
);
const [pilotFormMode, setPilotFormMode] = useState<FormMode>(FormMode.CANCEL);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenClosePilotForm = (mode: PilotFormMode, pilotId?: string) => {
const onOpenClosePilotForm = (mode: FormMode, pilotId?: string) => {
switch (mode) {
case PilotFormMode.ADD:
case PilotFormMode.EDIT:
case PilotFormMode.VIEW:
case FormMode.ADD:
case FormMode.EDIT:
case FormMode.VIEW:
setPilotFormMode(mode);
setSelectedPilotId(pilotId);
setIsDrawerOpen(true);
break;
case PilotFormMode.CANCEL:
case FormMode.CANCEL:
setPilotFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
@@ -49,64 +56,70 @@ const Pilots: React.FC<unknown> = () => {
}
};
type Pilot = {
partitionKey: string;
rowKey: string;
id: string;
name: string;
interface ActionMenuProps {
pilotId: 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 columns: TableColumnDef[] = [
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: ColumnDef<Pilot>[] = [
{
accessorKey: 'name',
header: 'Name'
},
{
id: 'actions',
header: 'Actions',
cellProps: {
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
cell: (info) => <ActionMenu pilotId={info.row.original.rowKey} />
}
];
@@ -131,31 +144,32 @@ const Pilots: React.FC<unknown> = () => {
}, []);
return (
<>
<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">
<div className="col-span-1">
<Typography variant="h2">Pilots</Typography>
</div>
<div className="col-span-1 justify-self-end">
<Box sx={{ margin: '20px' }}>
<Grid container spacing={2}>
<Grid size={11}>
<Typography variant="h4">Pilots</Typography>
</Grid>
<Grid display="flex" justifyContent="right" size={1}>
<Button
className="flex justify-center gap-3"
variant="filled"
onClick={() => onOpenClosePilotForm(PilotFormMode.ADD)}
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
startIcon={<PlusIcon />}
variant="contained"
data-testid="pilot-add-button"
>
<PlusIcon size="lg" />
Add Pilot
</Button>
</div>
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />}
</div>
</Grid>
<Grid size={12}>
{pilots.length > 0 && <Table columns={columns} data={pilots} />}
</Grid>
</Grid>
<PilotForm
isDrawerOpen={isDrawerOpen}
mode={pilotFormMode}
onOpenClose={(mode) => onOpenClosePilotForm(mode)}
pilotId={selectedPilotId}
/>
</>
</Box>
);
};

View File

@@ -1,19 +1,14 @@
import { useEffect, useState } from 'react';
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 {
Avatar,
Button,
IconButton,
Menu,
MenuHandler,
MenuItem,
MenuList,
Navbar,
NavbarBrand,
NavbarLinks,
NavbarMenu,
NavbarItemProps,
PlaneIcon,
SignOutIcon,
Spinner,
@@ -37,10 +32,15 @@ const SiteNav: React.FC<unknown> = () => {
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const { inProgress, instance } = useMsal();
const navItems: NavbarItemProps[] = [
const navigate = useNavigate();
const pages = [
{
name: 'Logbook',
url: '/'
},
{
name: 'Pilots',
url: '#'
url: '/pilots'
}
];
const handleSignIn = async () => {
@@ -83,6 +83,20 @@ const SiteNav: React.FC<unknown> = () => {
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(() => {
const callback = instance.addEventCallback(
@@ -150,107 +164,16 @@ const SiteNav: React.FC<unknown> = () => {
}, [isAuthenticated]);
return (
<Navbar className="mt-6">
<NavbarBrand>
<img height={40} width={40} src="noahspan-logo.png" />{' '}
<PlaneIcon size="2x" />
</NavbarBrand>
<NavbarLinks items={navItems} />
<NavbarMenu>
<div className="flex items-center gap-2 hidden lg:inline-block">
{!loading && isAuthenticated && (
<Menu placement="bottom-end">
<MenuHandler>
<div>
{userPhoto && (
<img
className="rounded-full cursor-pointer"
height="40"
width="40"
src={userPhoto}
<Navbar
handlePageClick={handlePageClick}
handleSignIn={handleSignIn}
isAuthenticated={isAuthenticated}
logo={<PlaneIcon size="2x" />}
pages={pages}
settings={<Settings />}
userPhoto={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;

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 {
body {
@apply bg-[#ECEFF1];
@apply bg-[#fafaf9];
@apply text-black;
}
}

View File

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