adding new logbook entry #24
2
api/.dockerignore
Normal file
2
api/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
test
|
||||||
50
api/Dockerfile
Normal file
50
api/Dockerfile
Normal 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"]
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/proxies",
|
|
||||||
"proxies": {}
|
|
||||||
}
|
|
||||||
@@ -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],
|
||||||
|
|||||||
41
api/src/logbook/logbook.controller.ts
Normal file
41
api/src/logbook/logbook.controller.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
api/src/logbook/logbook.dto.ts
Normal file
26
api/src/logbook/logbook.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
29
api/src/logbook/logbook.entity.ts
Normal file
29
api/src/logbook/logbook.entity.ts
Normal 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;
|
||||||
|
}
|
||||||
20
api/src/logbook/logbook.module.ts
Normal file
20
api/src/logbook/logbook.module.ts
Normal 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 {}
|
||||||
113
api/src/logbook/logbook.service.ts
Normal file
113
api/src/logbook/logbook.service.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { FormMode } from '../../enums/formMode';
|
||||||
|
|
||||||
|
export interface ILogbookEntryFormProps {
|
||||||
|
entryId?: string;
|
||||||
|
isDrawerOpen: boolean;
|
||||||
|
mode: FormMode;
|
||||||
|
onOpenClose: (mode: FormMode) => void;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,134 +1,134 @@
|
|||||||
import { IPilotFormCertificates } from './IPilotFormCertificates';
|
// import { IPilotFormCertificates } from './IPilotFormCertificates';
|
||||||
import {
|
// import {
|
||||||
Button,
|
// Button,
|
||||||
DatePicker,
|
// DatePicker,
|
||||||
Input,
|
// Input,
|
||||||
Option,
|
// Option,
|
||||||
PlusIcon,
|
// PlusIcon,
|
||||||
Select,
|
// Select,
|
||||||
TrashIcon,
|
// TrashIcon,
|
||||||
Typography
|
// Typography
|
||||||
} from '@noahspan/noahspan-components';
|
// } from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
|
// const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
|
||||||
certificates
|
// certificates
|
||||||
}: IPilotFormCertificates) => {
|
// }: IPilotFormCertificates) => {
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
formState: { errors },
|
// formState: { errors },
|
||||||
setValue
|
// setValue
|
||||||
} = useFormContext();
|
// } = useFormContext();
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
// const { fields, append, remove } = useFieldArray({
|
||||||
name: 'certificates',
|
// name: 'certificates',
|
||||||
control
|
// control
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
{fields.length > 0 && (
|
// {fields.length > 0 && (
|
||||||
<>
|
// <>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Typography variant="h6">Type</Typography>
|
// <Typography variant="h6">Type</Typography>
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Typography variant="h6">Number</Typography>
|
// <Typography variant="h6">Number</Typography>
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Typography variant="h6">Date of Issue</Typography>
|
// <Typography variant="h6">Date of Issue</Typography>
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1"></div>
|
// <div className="col-span-1"></div>
|
||||||
</>
|
// </>
|
||||||
)}
|
// )}
|
||||||
{fields.map((field, index) => {
|
// {fields.map((field, index) => {
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Controller
|
// <Controller
|
||||||
name={`certificates.${index}.type`}
|
// name={`certificates.${index}.type`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field }) => {
|
// render={({ field }) => {
|
||||||
return (
|
// return (
|
||||||
<Select label="Type" {...field}>
|
// <Select label="Type" {...field}>
|
||||||
<Option key="student" value="Student">
|
// <Option key="student" value="Student">
|
||||||
Student
|
// Student
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="private" value="Private">
|
// <Option key="private" value="Private">
|
||||||
Private
|
// Private
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="instrument" value="Instrument">
|
// <Option key="instrument" value="Instrument">
|
||||||
Instrument
|
// Instrument
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="recreational" value="Recreational">
|
// <Option key="recreational" value="Recreational">
|
||||||
Recreational
|
// Recreational
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="sport" value="Sport">
|
// <Option key="sport" value="Sport">
|
||||||
Sport
|
// Sport
|
||||||
</Option>
|
// </Option>
|
||||||
</Select>
|
// </Select>
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Controller
|
// <Controller
|
||||||
name={`certificates.${index}.number`}
|
// name={`certificates.${index}.number`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field }) => {
|
// render={({ field }) => {
|
||||||
return <Input label="Number" {...field} />;
|
// return <Input label="Number" {...field} />;
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Controller
|
// <Controller
|
||||||
name={`certificates.${index}.dateOfIssue`}
|
// name={`certificates.${index}.dateOfIssue`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field }) => {
|
// render={({ field }) => {
|
||||||
return (
|
// return (
|
||||||
<DatePicker
|
// <DatePicker
|
||||||
handleDateChanged={(date: string) => {
|
// handleDateChanged={(date: string) => {
|
||||||
setValue(`certificates.${index}.dateOfIssue`, date);
|
// setValue(`certificates.${index}.dateOfIssue`, date);
|
||||||
}}
|
// }}
|
||||||
inputProps={{
|
// inputProps={{
|
||||||
value: field.value
|
// value: field.value
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Button
|
// <Button
|
||||||
className="flex items-center gap-3"
|
// className="flex items-center gap-3"
|
||||||
onClick={() => remove(index)}
|
// onClick={() => remove(index)}
|
||||||
variant="outlined"
|
// variant="outlined"
|
||||||
>
|
// >
|
||||||
<TrashIcon size="lg" />
|
// <TrashIcon size="lg" />
|
||||||
Delete
|
// Delete
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
})}
|
// })}
|
||||||
<div className="col-span-4">
|
// <div className="col-span-4">
|
||||||
<Button
|
// <Button
|
||||||
className="flex items-center gap-3"
|
// className="flex items-center gap-3"
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
append({
|
// append({
|
||||||
type: '',
|
// type: '',
|
||||||
number: '',
|
// number: '',
|
||||||
dateOfIssue: null
|
// dateOfIssue: null
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
variant="outlined"
|
// variant="outlined"
|
||||||
>
|
// >
|
||||||
<PlusIcon size="lg" />
|
// <PlusIcon size="lg" />
|
||||||
Add Certificate
|
// Add Certificate
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
export default PilotFormCertificates;
|
// export default PilotFormCertificates;
|
||||||
|
|||||||
@@ -1,120 +1,120 @@
|
|||||||
import { IPilotFormEndorsements } from './IPilotFormEndorsements';
|
// import { IPilotFormEndorsements } from './IPilotFormEndorsements';
|
||||||
import {
|
// import {
|
||||||
Button,
|
// Button,
|
||||||
DatePicker,
|
// DatePicker,
|
||||||
Input,
|
// Input,
|
||||||
Option,
|
// Option,
|
||||||
PlusIcon,
|
// PlusIcon,
|
||||||
Select,
|
// Select,
|
||||||
TrashIcon,
|
// TrashIcon,
|
||||||
Typography
|
// Typography
|
||||||
} from '@noahspan/noahspan-components';
|
// } from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
|
// const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
|
||||||
endorsements
|
// endorsements
|
||||||
}: IPilotFormEndorsements) => {
|
// }: IPilotFormEndorsements) => {
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
formState: { errors },
|
// formState: { errors },
|
||||||
setValue
|
// setValue
|
||||||
} = useFormContext();
|
// } = useFormContext();
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
// const { fields, append, remove } = useFieldArray({
|
||||||
name: 'endorsements',
|
// name: 'endorsements',
|
||||||
control
|
// control
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
{fields.length > 0 && (
|
// {fields.length > 0 && (
|
||||||
<>
|
// <>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Typography variant="h6">Type</Typography>
|
// <Typography variant="h6">Type</Typography>
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Typography variant="h6">Date of Issue</Typography>
|
// <Typography variant="h6">Date of Issue</Typography>
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1"></div>
|
// <div className="col-span-1"></div>
|
||||||
<div className="col-span-1"></div>
|
// <div className="col-span-1"></div>
|
||||||
</>
|
// </>
|
||||||
)}
|
// )}
|
||||||
{fields.map((field, index) => {
|
// {fields.map((field, index) => {
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Controller
|
// <Controller
|
||||||
name={`endorsements.${index}.type`}
|
// name={`endorsements.${index}.type`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field }) => {
|
// render={({ field }) => {
|
||||||
return (
|
// return (
|
||||||
<Select label="Type" {...field}>
|
// <Select label="Type" {...field}>
|
||||||
<Option key="complex" value="Complex">
|
// <Option key="complex" value="Complex">
|
||||||
Complex
|
// Complex
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="highPerformance" value="High Performance">
|
// <Option key="highPerformance" value="High Performance">
|
||||||
High Performance
|
// High Performance
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="highAltitude" value="High Altitude">
|
// <Option key="highAltitude" value="High Altitude">
|
||||||
High Altitude
|
// High Altitude
|
||||||
</Option>
|
// </Option>
|
||||||
<Option key="tailwheel" value="Tailwheel">
|
// <Option key="tailwheel" value="Tailwheel">
|
||||||
Tailwheel
|
// Tailwheel
|
||||||
</Option>
|
// </Option>
|
||||||
</Select>
|
// </Select>
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Controller
|
// <Controller
|
||||||
name={`endorsements.${index}.dateOfIssue`}
|
// name={`endorsements.${index}.dateOfIssue`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field }) => {
|
// render={({ field }) => {
|
||||||
return (
|
// return (
|
||||||
<DatePicker
|
// <DatePicker
|
||||||
handleDateChanged={(date: string) => {
|
// handleDateChanged={(date: string) => {
|
||||||
setValue(`endorsements.${index}.dateOfIssue`, date);
|
// setValue(`endorsements.${index}.dateOfIssue`, date);
|
||||||
}}
|
// }}
|
||||||
inputProps={{
|
// inputProps={{
|
||||||
value: field.value
|
// value: field.value
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div className="col-span-1">
|
// <div className="col-span-1">
|
||||||
<Button
|
// <Button
|
||||||
className="flex items-center gap-3"
|
// className="flex items-center gap-3"
|
||||||
onClick={() => remove(index)}
|
// onClick={() => remove(index)}
|
||||||
variant="outlined"
|
// variant="outlined"
|
||||||
>
|
// >
|
||||||
<TrashIcon size="lg" />
|
// <TrashIcon size="lg" />
|
||||||
Delete
|
// Delete
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
})}
|
// })}
|
||||||
<div className="col-span-4">
|
// <div className="col-span-4">
|
||||||
<Button
|
// <Button
|
||||||
className="flex items-center gap-3"
|
// className="flex items-center gap-3"
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
append({
|
// append({
|
||||||
type: '',
|
// type: '',
|
||||||
number: '',
|
// number: '',
|
||||||
dateOfIssue: null
|
// dateOfIssue: null
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
variant="outlined"
|
// variant="outlined"
|
||||||
>
|
// >
|
||||||
<PlusIcon size="lg" />
|
// <PlusIcon size="lg" />
|
||||||
Add Endorsement
|
// Add Endorsement
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
export default PilotFormEndorsements;
|
// export default PilotFormEndorsements;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
6
app/src/enums/formMode.ts
Normal file
6
app/src/enums/formMode.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum FormMode {
|
||||||
|
ADD = 'ADD',
|
||||||
|
EDIT = 'EDIT',
|
||||||
|
VIEW = 'VIEW',
|
||||||
|
CANCEL = 'CANCEL'
|
||||||
|
}
|
||||||
52
app/src/hooks/pilots/UsePilots.tsx
Normal file
52
app/src/hooks/pilots/UsePilots.tsx
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-[#ECEFF1];
|
@apply bg-[#fafaf9];
|
||||||
@apply text-black;
|
@apply text-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
27
compose.yaml
Normal 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
4142
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user