adding new logbook entry (#24)
* adding new logbook entry * adding new logbook entry
This commit was merged in pull request #24.
This commit is contained in:
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/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@noahspan/noahspan-modules": "^0.4.0",
|
||||
"@noahspan/noahspan-modules": "^0.4.7",
|
||||
"@schematics/angular": "^17.3.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/proxies",
|
||||
"proxies": {}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@noahspan/noahspan-modules';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { LogbookModule } from './logbook/logbook.module';
|
||||
import { PilotModule } from './pilot/pilot.module';
|
||||
import { APP_FILTER } from '@nestjs/core';
|
||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
@@ -32,6 +33,7 @@ import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
clientId: process.env.CLIENT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET
|
||||
}),
|
||||
LogbookModule,
|
||||
PilotModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
|
||||
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,6 +82,7 @@ export class PilotInfoService {
|
||||
}
|
||||
|
||||
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
|
||||
try {
|
||||
const client: TableClient =
|
||||
await this.tableService.getTableClient('Pilots');
|
||||
const pilotInfo: PilotInfoEntity = new PilotInfoEntity();
|
||||
@@ -89,12 +90,11 @@ export class PilotInfoService {
|
||||
Object.assign(pilotInfo, pilotInfoData);
|
||||
pilotInfo.partitionKey = 'pilot';
|
||||
pilotInfo.rowKey = pilotInfo.id;
|
||||
|
||||
try {
|
||||
console.log(pilotInfo);
|
||||
return await client.createEntity(pilotInfo);
|
||||
} catch (error) {
|
||||
const restError: RestError = error as RestError;
|
||||
|
||||
console.log(restError);
|
||||
throw new CustomError(
|
||||
restError.details['odataError']['message']['value'],
|
||||
restError.details['odataError']['code'],
|
||||
|
||||
@@ -7,7 +7,11 @@ import { PilotInfoService } from './info/pilot-info.service';
|
||||
imports: [
|
||||
TableModule.register({
|
||||
accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME,
|
||||
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY
|
||||
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
|
||||
accountUrl: process.env.AZURE_STORAGE_ACCOUNT_URL,
|
||||
allowInsecureConnection: Boolean(
|
||||
process.env.AZURE_STORAGE_ALLOW_INSECURE_CONNECTION
|
||||
)
|
||||
})
|
||||
],
|
||||
controllers: [PilotController],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@noahspan/noahspan-components": "^0.7.0",
|
||||
"@noahspan/noahspan-components": "^0.8.9",
|
||||
"axios": "^1.7.2",
|
||||
"framer-motion": "^11.1.7",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Pilots from './components/pilots/Pilots';
|
||||
import Logbook from './components/logbook/Logbook';
|
||||
import { useAppContext } from './hooks/appContext/UseAppContext';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
|
||||
@@ -35,12 +36,13 @@ const App: React.FC<unknown> = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<div>
|
||||
<SiteNav />
|
||||
<Routes>
|
||||
{useFeatureFlag('flying-pilots')?.enabled && (
|
||||
<Route path="/" element={<Pilots />} />
|
||||
<Route path="/pilots" element={<Pilots />} />
|
||||
)}
|
||||
<Route path="/" element={<Logbook />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,351 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import LogbookEntryForm from '../logbookEntryForm/LogbookEntryForm';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ColumnDef,
|
||||
Grid,
|
||||
PlusIcon,
|
||||
Table,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
|
||||
type LogbookEntry = {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
pilotId: string;
|
||||
date: string;
|
||||
aircraftMakeModel: string;
|
||||
aircraftIdentity: string;
|
||||
routeFrom: string;
|
||||
routeTo: string;
|
||||
durationOfFlight: number | null;
|
||||
singleEngineLand: number | null;
|
||||
simulatorAtd: number | null;
|
||||
landingsDay: number | null;
|
||||
landingsNight: number | null;
|
||||
instrumentActual: number | null;
|
||||
instrumentSimulated: number | null;
|
||||
instrumentApproaches: number | null;
|
||||
instrumentHolds: number | null;
|
||||
instrumentNavTrack: number | null;
|
||||
groundTrainingReceived: number;
|
||||
flightTrainingReceived: number;
|
||||
crossCountry: number | null;
|
||||
night: number | null;
|
||||
solo: number | null;
|
||||
pilotInCommand: number | null;
|
||||
};
|
||||
|
||||
const Logbook: React.FC<unknown> = () => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [entryFormMode, setEntryFormMode] = useState<FormMode>(FormMode.CANCEL);
|
||||
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
|
||||
const [entries, setEntries] = useState([]);
|
||||
const onOpenCloseEntryForm = (mode: FormMode, pilotId?: string) => {
|
||||
switch (mode) {
|
||||
case FormMode.ADD:
|
||||
case FormMode.EDIT:
|
||||
case FormMode.VIEW:
|
||||
setEntryFormMode(mode);
|
||||
setSelectedPilotId(pilotId);
|
||||
setIsDrawerOpen(true);
|
||||
break;
|
||||
case FormMode.CANCEL:
|
||||
setEntryFormMode(mode);
|
||||
setSelectedPilotId(undefined);
|
||||
setIsDrawerOpen(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<LogbookEntry>[] = [
|
||||
{
|
||||
accessorKey: 'pilotName',
|
||||
header: 'Pilot'
|
||||
},
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Date'
|
||||
},
|
||||
{
|
||||
accessorKey: 'aircraftMakeModel',
|
||||
header: 'Aircraft Make & Model'
|
||||
},
|
||||
{
|
||||
accessorKey: 'aircraftIdentity',
|
||||
header: 'Aircraft Identity'
|
||||
},
|
||||
{
|
||||
id: 'route',
|
||||
header: 'Route of Flight',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'routeFrom',
|
||||
header: 'From'
|
||||
},
|
||||
{
|
||||
accessorKey: 'routeTo',
|
||||
header: 'To'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
accessorKey: 'durationOfFlight',
|
||||
header: 'Duration Of Flight',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()!.toString()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'singleEngineLand',
|
||||
header: 'Single Engine Land',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()!.toString()).toFixed(1)
|
||||
: null
|
||||
},
|
||||
{
|
||||
id: 'landings',
|
||||
header: 'Landings',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'landingsDay',
|
||||
header: 'Day',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'landingsNight',
|
||||
header: 'Night',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'instrument',
|
||||
header: 'Instrument',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'instrumentActual',
|
||||
header: 'Actual',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'instrumentSimulated',
|
||||
header: 'Simulated',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'instrumentApproaches',
|
||||
header: 'Approaches',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'instrumentHolds',
|
||||
header: 'Holds',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'instrumentNavTrack',
|
||||
header: 'Nav/Track',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'experienceTraining',
|
||||
header: 'Type of pilot experience or training',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
accessorKey: 'groundTrainingReceived',
|
||||
header: 'Ground Training Received',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'flightTrainingReceived',
|
||||
header: 'Flight Training Received',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'crossCountry',
|
||||
header: 'Cross Country',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'night',
|
||||
header: 'Night',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'solo',
|
||||
header: 'Solo',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
},
|
||||
{
|
||||
accessorKey: 'pilotInCommand',
|
||||
header: 'Pilot In Command',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info) =>
|
||||
info.getValue() !== null
|
||||
? parseFloat(info.getValue()).toFixed(1)
|
||||
: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
accessorKey: 'notes',
|
||||
header: 'Notes'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const getLogbookEntries = async () => {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const config = isAuthenticated
|
||||
? { headers: { Authorization: `${token}` } }
|
||||
: {};
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/logbook`,
|
||||
config
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
setEntries(response.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (isAuthenticated && !isDrawerOpen) {
|
||||
getLogbookEntries();
|
||||
}
|
||||
}, [isAuthenticated, isDrawerOpen]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Logbook goes here</div>
|
||||
</div>
|
||||
<Box sx={{ margin: '20px' }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<Typography variant="h4">Logbook</Typography>
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="right" size={1}>
|
||||
<Button
|
||||
onClick={() => onOpenCloseEntryForm(FormMode.ADD)}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
data-testid="pilot-add-button"
|
||||
>
|
||||
Add Entry
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
{entries.length > 0 && <Table columns={columns} data={entries} />}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<LogbookEntryForm
|
||||
entryId={'1'}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
mode={entryFormMode}
|
||||
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
// Accordion,
|
||||
// AccordionItem,
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Input
|
||||
// } from '@nextui-org/react';
|
||||
// import { useForm, Controller, SubmitHandler } from 'react-hook-form';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Button,
|
||||
ChevronDownIcon,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Grid,
|
||||
IconButton,
|
||||
SaveIcon,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
XmarkIcon
|
||||
} from '@noahspan/noahspan-components';
|
||||
import {
|
||||
useForm,
|
||||
Controller,
|
||||
SubmitHandler,
|
||||
FormProvider
|
||||
} from 'react-hook-form';
|
||||
import { ILogbookEntryFormProps } from './ILogbookEntryFormProps';
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { usePilots } from '../../hooks/pilots/UsePilots';
|
||||
|
||||
// const LogbookEntryForm: React.FC<unknown> = () => {
|
||||
// const { control, handleSubmit } = useForm();
|
||||
const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
|
||||
entryId,
|
||||
isDrawerOpen,
|
||||
mode,
|
||||
onOpenClose
|
||||
}) => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [pilotOptions, setPilotOptions] = useState<
|
||||
{ label: string; value: string }[]
|
||||
>([]);
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const defaultValues = {
|
||||
partitionKey: '',
|
||||
rowKey: '',
|
||||
pilotId: '',
|
||||
pilotName: '',
|
||||
date: null,
|
||||
aircraftMakeModel: '',
|
||||
aircraftIdentity: '',
|
||||
routeFrom: '',
|
||||
routeTo: '',
|
||||
durationOfFlight: null,
|
||||
singleEngineLand: null,
|
||||
simulatorAtd: null,
|
||||
landingsDay: null,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: null,
|
||||
night: null,
|
||||
solo: null,
|
||||
pilotInCommand: null,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: ''
|
||||
};
|
||||
const methods = useForm();
|
||||
const { pilots } = usePilots();
|
||||
|
||||
// const onSubmit = (data: unknown) => {
|
||||
// console.log(data);
|
||||
// };
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
};
|
||||
|
||||
// return (
|
||||
// <form className="m-10" onSubmit={handleSubmit(onSubmit)}>
|
||||
// <div className="grid grid-cols-2 gap-4">
|
||||
// <div className="self-center">
|
||||
// <label>Date</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="date"
|
||||
// control={control}
|
||||
// render={({ field }) => <DatePicker labelPlacement="outside-left" />}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="self-center">
|
||||
// <label>Aircraft Type</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="aircraftType"
|
||||
// control={control}
|
||||
// render={({ field }) => (
|
||||
// <Input fullWidth={true} labelPlacement="outside-left" />
|
||||
// )}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="self-center">
|
||||
// <label>Aircraft Identity</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="aircraftIdent"
|
||||
// control={control}
|
||||
// render={({ field }) => (
|
||||
// <Input fullWidth={true} labelPlacement="outside-left" />
|
||||
// )}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="self-center">
|
||||
// <label>Route To</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="routeTo"
|
||||
// control={control}
|
||||
// render={({ field }) => <Input />}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="self-center">
|
||||
// <label>Route From</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="routeFrom"
|
||||
// control={control}
|
||||
// render={({ field }) => <Input />}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="self-center">
|
||||
// <label>Duration of Flight</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="flightDuration"
|
||||
// control={control}
|
||||
// render={({ field }) => <Input />}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="self-center">
|
||||
// <label>Single Engine Land</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="aircraftSEL"
|
||||
// control={control}
|
||||
// render={({ field }) => <Input />}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="col-span-2">
|
||||
// <Accordion>
|
||||
// <AccordionItem title="Aircraft Category and Class">
|
||||
// <div className="self-center">
|
||||
// <label>Single Engine Land</label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="aircraftSEL"
|
||||
// control={control}
|
||||
// render={({ field }) => <Input />}
|
||||
// />
|
||||
// </div>
|
||||
// </AccordionItem>
|
||||
// </Accordion>
|
||||
// </div>
|
||||
// <div className="col-span-2 justify-self-end">
|
||||
// <Button color="default">Cancel</Button>
|
||||
// <Button className="ml-10" color="primary">
|
||||
// Save
|
||||
// </Button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// };
|
||||
const onSubmit = async (data: unknown) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// export default LogbookEntryForm;
|
||||
const accessToken: string = await getAccessToken();
|
||||
|
||||
await httpClient.post(`api/logbook`, data, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
|
||||
methods.reset(defaultValues);
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errResp = error.response;
|
||||
|
||||
console.log(errResp?.data.message);
|
||||
} else {
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pilots) {
|
||||
const newPilotsOptions = pilots.map((pilot) => {
|
||||
return {
|
||||
label: pilot.name,
|
||||
value: pilot.id
|
||||
};
|
||||
});
|
||||
|
||||
setPilotOptions(newPilotsOptions);
|
||||
}
|
||||
}, [pilots]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
anchor="right"
|
||||
PaperProps={{
|
||||
sx: {
|
||||
padding: '30px',
|
||||
width: '33%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</Typography>
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="right" size={1}>
|
||||
<IconButton onClick={onCancel}>
|
||||
<XmarkIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Pilot *</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="pilotId"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
fullWidth
|
||||
onChange={(event) => {
|
||||
const pilot = pilots?.find(
|
||||
(pilot) => pilot.id === event.target.value
|
||||
);
|
||||
|
||||
if (pilot) {
|
||||
methods.setValue('pilotName', pilot.name);
|
||||
}
|
||||
|
||||
methods.setValue('pilotId', event.target.value);
|
||||
}}
|
||||
options={
|
||||
pilotOptions && pilotOptions.length > 0
|
||||
? pilotOptions
|
||||
: []
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Date *</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="date"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<DatePicker onChange={onChange} value={value} />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Aircraft Make and Model *</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="aircraftMakeModel"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Aircraft Identity *</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="aircraftIdentity"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Route From</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="routeFrom"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Route To</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="routeTo"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Duration Of Flight</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="durationOfFlight"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Single Engine Land</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="singleEngineLand"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Simulator or ATD</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="simulatorAtd"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ChevronDownIcon />}>
|
||||
<Typography variant="body1">Landings</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Day</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="landingsDay"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Night</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="landingsNight"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ChevronDownIcon />}>
|
||||
Instrument
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Actual</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="instrumentActual"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Simulated</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="instrumentSimulated"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">
|
||||
Instrument Approaches
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="instrumentApproaches"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Holds</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="instrumentHolds"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Nav / Track</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="instrumentNavTrack"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ChevronDownIcon />}>
|
||||
Type of Pilot Experience or Training
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">
|
||||
Ground Training Received
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="groundTrainingReceived"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">
|
||||
Flight Training Received
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="flightTrainingReceived"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Cross Country</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="crossCountry"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Night</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="night"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Solo</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="solo"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Pilot in Command</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="pilotInCommand"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography variant="body1">Notes</Typography>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<Controller
|
||||
name="notes"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
// disabled={isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
multiline
|
||||
rows={3}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid display="flex" gap={2} justifyContent="right" size={12}>
|
||||
<Button
|
||||
// disabled={isDisabled}
|
||||
startIcon={<XmarkIcon />}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
data-testid="pilot-cancel-button"
|
||||
size="small"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
// disabled={isDisabled}
|
||||
startIcon={<SaveIcon />}
|
||||
size="small"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogbookEntryForm;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { PilotFormMode } from './PilotForm';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
|
||||
export interface IPilotFormProps {
|
||||
isDrawerOpen: boolean;
|
||||
mode: PilotFormMode;
|
||||
onOpenClose: (mode: PilotFormMode) => void;
|
||||
mode: FormMode;
|
||||
onOpenClose: (mode: FormMode) => void;
|
||||
pilotId?: string;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
Input,
|
||||
Grid,
|
||||
IconButton,
|
||||
PeoplePicker,
|
||||
SaveIcon,
|
||||
StateSelect,
|
||||
TextField,
|
||||
Typography,
|
||||
XmarkIcon
|
||||
} from '@noahspan/noahspan-components';
|
||||
@@ -18,13 +17,8 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
|
||||
export enum PilotFormMode {
|
||||
ADD = 'ADD',
|
||||
EDIT = 'EDIT',
|
||||
VIEW = 'VIEW',
|
||||
CANCEL = 'CANCEL'
|
||||
}
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Person } from '@microsoft/microsoft-graph-types';
|
||||
|
||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
pilotId,
|
||||
@@ -36,6 +30,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
|
||||
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
|
||||
useState<boolean>(false);
|
||||
const [selectedPerson, setSelectedPerson] = useState<Person>({
|
||||
userPrincipalName: '',
|
||||
displayName: ''
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
@@ -56,25 +54,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
});
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||
|
||||
const handlePeoplePickerOnClick = (
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
const divElement: HTMLDivElement = event.target as HTMLDivElement;
|
||||
|
||||
methods.setValue('id', divElement.id);
|
||||
methods.setValue('name', divElement.textContent!);
|
||||
setPeoplePickerResults([]);
|
||||
};
|
||||
|
||||
const handlePeoplePickerOnChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
const onPeoplePickerSearch = async (
|
||||
_event: React.SyntheticEvent,
|
||||
value: string
|
||||
) => {
|
||||
setIsPeoplePickerLoading(true);
|
||||
|
||||
try {
|
||||
methods.setValue('name', event.target.value);
|
||||
|
||||
const searchString: string = event.target.value;
|
||||
if (value !== '') {
|
||||
const searchString: string = value;
|
||||
const accessToken: string = await getAccessToken();
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/personSearch?search=${searchString}`,
|
||||
@@ -84,8 +72,11 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(response);
|
||||
|
||||
setPeoplePickerResults(response.data);
|
||||
} else {
|
||||
setPeoplePickerResults([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
@@ -93,9 +84,20 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onPeoplePickerSelectionChange = (
|
||||
_event: React.SyntheticEvent,
|
||||
value: Person,
|
||||
_reason: string
|
||||
) => {
|
||||
methods.setValue('id', value.userPrincipalName!.toString());
|
||||
methods.setValue('name', value.displayName!.toString());
|
||||
setSelectedPerson(value);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
onOpenClose(PilotFormMode.CANCEL);
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
setSelectedPerson({ userPrincipalName: '', displayName: '' });
|
||||
};
|
||||
|
||||
const onSubmit = async (data: unknown) => {
|
||||
@@ -127,7 +129,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === PilotFormMode.VIEW) {
|
||||
if (mode === FormMode.VIEW) {
|
||||
setIsDisabled(true);
|
||||
}
|
||||
}, [mode]);
|
||||
@@ -146,10 +148,14 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
);
|
||||
const pilot = response.data;
|
||||
console.log(pilot);
|
||||
// methods.setValue('blah', pilot.value)
|
||||
setSelectedPerson({
|
||||
userPrincipalName: pilot.id,
|
||||
displayName: pilot.name
|
||||
});
|
||||
methods.reset(pilot);
|
||||
console.log(methods.getValues());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -163,119 +169,91 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
return (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
placement="right"
|
||||
size={1000}
|
||||
anchor="right"
|
||||
data-testid="pilot-drawer"
|
||||
PaperProps={{
|
||||
sx: {
|
||||
padding: '30px',
|
||||
width: '33%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<DrawerHeader text="Add Pilot" onClose={onCancel} />
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
<DrawerBody>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="col-span-1">
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</Typography>
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="right" size={1}>
|
||||
<IconButton onClick={onCancel}>
|
||||
<XmarkIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">Name *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
name="name"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A name must be selected' }}
|
||||
render={({ field: { value } }) => (
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<PeoplePicker
|
||||
results={peoplePickerResults}
|
||||
inputProps={{
|
||||
disabled: isDisabled,
|
||||
labelProps: {
|
||||
className: 'before:content-none after:content-none'
|
||||
},
|
||||
onChange: (event) => handlePeoplePickerOnChange(event),
|
||||
error: methods.formState.errors.name ? true : false,
|
||||
helperText: methods.formState.errors.name
|
||||
? methods.formState.errors.name.message?.toString()
|
||||
: undefined,
|
||||
value: value
|
||||
}}
|
||||
listItemProps={{
|
||||
children: null,
|
||||
onClick: handlePeoplePickerOnClick
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
loading={isPeoplePickerLoading}
|
||||
data-testid="pilot-form-people-picker"
|
||||
onInputChanged={onPeoplePickerSearch}
|
||||
onSelectionChanged={onPeoplePickerSelectionChange}
|
||||
options={peoplePickerResults}
|
||||
value={selectedPerson}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">Address *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Controller
|
||||
name="address"
|
||||
control={methods.control}
|
||||
rules={{ required: 'An address is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
className="!border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
<TextField
|
||||
disabled={isDisabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
? methods.formState.errors.address.message
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-address-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">City *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Controller
|
||||
name="city"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A city is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
<TextField
|
||||
disabled={isDisabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.city ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.city
|
||||
? methods.formState.errors.city.message?.toString()
|
||||
? methods.formState.errors.city.message
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-city-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">State *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Controller
|
||||
name="state"
|
||||
control={methods.control}
|
||||
@@ -283,15 +261,13 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<StateSelect
|
||||
disabled={isDisabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={methods.formState.errors.state ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.state
|
||||
? methods.formState.errors.state.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
// error={methods.formState.errors.state ? true : false}
|
||||
fullWidth
|
||||
// helperText={
|
||||
// methods.formState.errors.state
|
||||
// ? methods.formState.errors.state.message?.toString()
|
||||
// : undefined
|
||||
// }
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
variant="outlined"
|
||||
@@ -299,48 +275,35 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">Postal Code *</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Controller
|
||||
name="postalCode"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A postal code is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
<TextField
|
||||
disabled={isDisabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
error={
|
||||
methods.formState.errors.postalCode ? true : false
|
||||
}
|
||||
error={methods.formState.errors.postalCode ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.postalCode
|
||||
? methods.formState.errors.postalCode.message?.toString()
|
||||
? methods.formState.errors.postalCode.message
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-postal-code-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">Email</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Controller
|
||||
name="email"
|
||||
control={methods.control}
|
||||
@@ -351,32 +314,25 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
<TextField
|
||||
disabled={isDisabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
fullWidth
|
||||
error={methods.formState.errors.email ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.email
|
||||
? methods.formState.errors.email.message?.toString()
|
||||
? methods.formState.errors.email.message
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-email-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Typography variant="h6">Phone Number</Typography>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Controller
|
||||
name="phone"
|
||||
control={methods.control}
|
||||
@@ -387,26 +343,44 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
<TextField
|
||||
disabled={isDisabled}
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none'
|
||||
}}
|
||||
fullWidth
|
||||
error={methods.formState.errors.phone ? true : false}
|
||||
helperText={
|
||||
methods.formState.errors.phone
|
||||
? methods.formState.errors.phone.message?.toString()
|
||||
? methods.formState.errors.phone.message
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-phone-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid display="flex" gap={2} justifyContent="right" size={12}>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
startIcon={<XmarkIcon />}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
data-testid="pilot-cancel-button"
|
||||
size="small"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
startIcon={<SaveIcon />}
|
||||
size="small"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* {pilotId && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
@@ -512,41 +486,34 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
<PilotFormEndorsements endorsements={[]} />
|
||||
</>
|
||||
)} */}
|
||||
</div>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<>
|
||||
{mode !== PilotFormMode.VIEW && (
|
||||
{/* </div>
|
||||
{mode !== FormMode.VIEW && (
|
||||
<div className="flex gap-2 justify-end justify-self-center pt-4">
|
||||
<div>
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
disabled={isDisabled}
|
||||
variant="outlined"
|
||||
startIcon={<XmarkIcon />}
|
||||
variant='contained'
|
||||
onClick={onCancel}
|
||||
data-testid="pilot-cancel-button"
|
||||
>
|
||||
<XmarkIcon size="lg" />
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
variant="filled"
|
||||
startIcon={<SaveIcon />}
|
||||
size='medium'
|
||||
type="submit"
|
||||
variant='contained'
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
<SaveIcon size="lg" />
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</DrawerFooter>
|
||||
)} */}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,134 +1,134 @@
|
||||
import { IPilotFormCertificates } from './IPilotFormCertificates';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Input,
|
||||
Option,
|
||||
PlusIcon,
|
||||
Select,
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
// import { IPilotFormCertificates } from './IPilotFormCertificates';
|
||||
// import {
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Input,
|
||||
// Option,
|
||||
// PlusIcon,
|
||||
// Select,
|
||||
// TrashIcon,
|
||||
// Typography
|
||||
// } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
|
||||
certificates
|
||||
}: IPilotFormCertificates) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
// const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
|
||||
// certificates
|
||||
// }: IPilotFormCertificates) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// setValue
|
||||
// } = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'certificates',
|
||||
control
|
||||
});
|
||||
// const { fields, append, remove } = useFieldArray({
|
||||
// name: 'certificates',
|
||||
// control
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Type</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Number</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Date of Issue</Typography>
|
||||
</div>
|
||||
<div className="col-span-1"></div>
|
||||
</>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`certificates.${index}.type`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select label="Type" {...field}>
|
||||
<Option key="student" value="Student">
|
||||
Student
|
||||
</Option>
|
||||
<Option key="private" value="Private">
|
||||
Private
|
||||
</Option>
|
||||
<Option key="instrument" value="Instrument">
|
||||
Instrument
|
||||
</Option>
|
||||
<Option key="recreational" value="Recreational">
|
||||
Recreational
|
||||
</Option>
|
||||
<Option key="sport" value="Sport">
|
||||
Sport
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`certificates.${index}.number`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return <Input label="Number" {...field} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`certificates.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
handleDateChanged={(date: string) => {
|
||||
setValue(`certificates.${index}.dateOfIssue`, date);
|
||||
}}
|
||||
inputProps={{
|
||||
value: field.value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => remove(index)}
|
||||
variant="outlined"
|
||||
>
|
||||
<TrashIcon size="lg" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<div className="col-span-4">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
number: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Certificate
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <>
|
||||
// {fields.length > 0 && (
|
||||
// <>
|
||||
// <div className="col-span-1">
|
||||
// <Typography variant="h6">Type</Typography>
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Typography variant="h6">Number</Typography>
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Typography variant="h6">Date of Issue</Typography>
|
||||
// </div>
|
||||
// <div className="col-span-1"></div>
|
||||
// </>
|
||||
// )}
|
||||
// {fields.map((field, index) => {
|
||||
// return (
|
||||
// <>
|
||||
// <div className="col-span-1">
|
||||
// <Controller
|
||||
// name={`certificates.${index}.type`}
|
||||
// control={control}
|
||||
// render={({ field }) => {
|
||||
// return (
|
||||
// <Select label="Type" {...field}>
|
||||
// <Option key="student" value="Student">
|
||||
// Student
|
||||
// </Option>
|
||||
// <Option key="private" value="Private">
|
||||
// Private
|
||||
// </Option>
|
||||
// <Option key="instrument" value="Instrument">
|
||||
// Instrument
|
||||
// </Option>
|
||||
// <Option key="recreational" value="Recreational">
|
||||
// Recreational
|
||||
// </Option>
|
||||
// <Option key="sport" value="Sport">
|
||||
// Sport
|
||||
// </Option>
|
||||
// </Select>
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Controller
|
||||
// name={`certificates.${index}.number`}
|
||||
// control={control}
|
||||
// render={({ field }) => {
|
||||
// return <Input label="Number" {...field} />;
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Controller
|
||||
// name={`certificates.${index}.dateOfIssue`}
|
||||
// control={control}
|
||||
// render={({ field }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// handleDateChanged={(date: string) => {
|
||||
// setValue(`certificates.${index}.dateOfIssue`, date);
|
||||
// }}
|
||||
// inputProps={{
|
||||
// value: field.value
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Button
|
||||
// className="flex items-center gap-3"
|
||||
// onClick={() => remove(index)}
|
||||
// variant="outlined"
|
||||
// >
|
||||
// <TrashIcon size="lg" />
|
||||
// Delete
|
||||
// </Button>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// })}
|
||||
// <div className="col-span-4">
|
||||
// <Button
|
||||
// className="flex items-center gap-3"
|
||||
// onClick={() => {
|
||||
// append({
|
||||
// type: '',
|
||||
// number: '',
|
||||
// dateOfIssue: null
|
||||
// });
|
||||
// }}
|
||||
// variant="outlined"
|
||||
// >
|
||||
// <PlusIcon size="lg" />
|
||||
// Add Certificate
|
||||
// </Button>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default PilotFormCertificates;
|
||||
// export default PilotFormCertificates;
|
||||
|
||||
@@ -1,120 +1,120 @@
|
||||
import { IPilotFormEndorsements } from './IPilotFormEndorsements';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Input,
|
||||
Option,
|
||||
PlusIcon,
|
||||
Select,
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
// import { IPilotFormEndorsements } from './IPilotFormEndorsements';
|
||||
// import {
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Input,
|
||||
// Option,
|
||||
// PlusIcon,
|
||||
// Select,
|
||||
// TrashIcon,
|
||||
// Typography
|
||||
// } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
|
||||
endorsements
|
||||
}: IPilotFormEndorsements) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
// const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
|
||||
// endorsements
|
||||
// }: IPilotFormEndorsements) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// setValue
|
||||
// } = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'endorsements',
|
||||
control
|
||||
});
|
||||
// const { fields, append, remove } = useFieldArray({
|
||||
// name: 'endorsements',
|
||||
// control
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Type</Typography>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h6">Date of Issue</Typography>
|
||||
</div>
|
||||
<div className="col-span-1"></div>
|
||||
<div className="col-span-1"></div>
|
||||
</>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`endorsements.${index}.type`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select label="Type" {...field}>
|
||||
<Option key="complex" value="Complex">
|
||||
Complex
|
||||
</Option>
|
||||
<Option key="highPerformance" value="High Performance">
|
||||
High Performance
|
||||
</Option>
|
||||
<Option key="highAltitude" value="High Altitude">
|
||||
High Altitude
|
||||
</Option>
|
||||
<Option key="tailwheel" value="Tailwheel">
|
||||
Tailwheel
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
name={`endorsements.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
handleDateChanged={(date: string) => {
|
||||
setValue(`endorsements.${index}.dateOfIssue`, date);
|
||||
}}
|
||||
inputProps={{
|
||||
value: field.value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => remove(index)}
|
||||
variant="outlined"
|
||||
>
|
||||
<TrashIcon size="lg" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<div className="col-span-4">
|
||||
<Button
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
number: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Endorsement
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <>
|
||||
// {fields.length > 0 && (
|
||||
// <>
|
||||
// <div className="col-span-1">
|
||||
// <Typography variant="h6">Type</Typography>
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Typography variant="h6">Date of Issue</Typography>
|
||||
// </div>
|
||||
// <div className="col-span-1"></div>
|
||||
// <div className="col-span-1"></div>
|
||||
// </>
|
||||
// )}
|
||||
// {fields.map((field, index) => {
|
||||
// return (
|
||||
// <>
|
||||
// <div className="col-span-1">
|
||||
// <Controller
|
||||
// name={`endorsements.${index}.type`}
|
||||
// control={control}
|
||||
// render={({ field }) => {
|
||||
// return (
|
||||
// <Select label="Type" {...field}>
|
||||
// <Option key="complex" value="Complex">
|
||||
// Complex
|
||||
// </Option>
|
||||
// <Option key="highPerformance" value="High Performance">
|
||||
// High Performance
|
||||
// </Option>
|
||||
// <Option key="highAltitude" value="High Altitude">
|
||||
// High Altitude
|
||||
// </Option>
|
||||
// <Option key="tailwheel" value="Tailwheel">
|
||||
// Tailwheel
|
||||
// </Option>
|
||||
// </Select>
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Controller
|
||||
// name={`endorsements.${index}.dateOfIssue`}
|
||||
// control={control}
|
||||
// render={({ field }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// handleDateChanged={(date: string) => {
|
||||
// setValue(`endorsements.${index}.dateOfIssue`, date);
|
||||
// }}
|
||||
// inputProps={{
|
||||
// value: field.value
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="col-span-1">
|
||||
// <Button
|
||||
// className="flex items-center gap-3"
|
||||
// onClick={() => remove(index)}
|
||||
// variant="outlined"
|
||||
// >
|
||||
// <TrashIcon size="lg" />
|
||||
// Delete
|
||||
// </Button>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// })}
|
||||
// <div className="col-span-4">
|
||||
// <Button
|
||||
// className="flex items-center gap-3"
|
||||
// onClick={() => {
|
||||
// append({
|
||||
// type: '',
|
||||
// number: '',
|
||||
// dateOfIssue: null
|
||||
// });
|
||||
// }}
|
||||
// variant="outlined"
|
||||
// >
|
||||
// <PlusIcon size="lg" />
|
||||
// Add Endorsement
|
||||
// </Button>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default PilotFormEndorsements;
|
||||
// export default PilotFormEndorsements;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PilotForm from '../pilotForm/PilotForm';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ColumnDef,
|
||||
EllipsisVerticalIcon,
|
||||
EyeIcon,
|
||||
Grid,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
PenIcon,
|
||||
PlusIcon,
|
||||
Table,
|
||||
TableColumnDef,
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
@@ -20,28 +22,33 @@ import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
import { PilotFormMode } from '../pilotForm/PilotForm';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
|
||||
type Pilot = {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const Pilots: React.FC<unknown> = () => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [pilotFormMode, setPilotFormMode] = useState<PilotFormMode>(
|
||||
PilotFormMode.CANCEL
|
||||
);
|
||||
const [pilotFormMode, setPilotFormMode] = useState<FormMode>(FormMode.CANCEL);
|
||||
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
|
||||
const [pilots, setPilots] = useState<Pilot[]>([]);
|
||||
const onOpenClosePilotForm = (mode: PilotFormMode, pilotId?: string) => {
|
||||
const onOpenClosePilotForm = (mode: FormMode, pilotId?: string) => {
|
||||
switch (mode) {
|
||||
case PilotFormMode.ADD:
|
||||
case PilotFormMode.EDIT:
|
||||
case PilotFormMode.VIEW:
|
||||
case FormMode.ADD:
|
||||
case FormMode.EDIT:
|
||||
case FormMode.VIEW:
|
||||
setPilotFormMode(mode);
|
||||
setSelectedPilotId(pilotId);
|
||||
setIsDrawerOpen(true);
|
||||
break;
|
||||
case PilotFormMode.CANCEL:
|
||||
case FormMode.CANCEL:
|
||||
setPilotFormMode(mode);
|
||||
setSelectedPilotId(undefined);
|
||||
setIsDrawerOpen(false);
|
||||
@@ -49,64 +56,70 @@ const Pilots: React.FC<unknown> = () => {
|
||||
}
|
||||
};
|
||||
|
||||
type Pilot = {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
name: string;
|
||||
interface ActionMenuProps {
|
||||
pilotId: string;
|
||||
}
|
||||
|
||||
const ActionMenu = ({ pilotId }: ActionMenuProps) => {
|
||||
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
||||
null
|
||||
);
|
||||
|
||||
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
console.log(event);
|
||||
setAnchorElAction(event.currentTarget);
|
||||
};
|
||||
|
||||
const columns: TableColumnDef[] = [
|
||||
const onCloseActionMenu = () => {
|
||||
setAnchorElAction(null);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<IconButton onClick={onOpenActionMenu}>
|
||||
<EllipsisVerticalIcon size="sm" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorElAction}
|
||||
keepMounted
|
||||
open={Boolean(anchorElAction)}
|
||||
onClose={onCloseActionMenu}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => onOpenClosePilotForm(FormMode.EDIT, pilotId)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PenIcon size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => onOpenClosePilotForm(FormMode.VIEW, pilotId)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EyeIcon size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>View</ListItemText>
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<TrashIcon size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<Pilot>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cellProps: {
|
||||
className: 'text-end'
|
||||
},
|
||||
cell: (info: any) => {
|
||||
const pilotId = info.row.original.rowKey;
|
||||
return (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<div>
|
||||
<IconButton variant="text">
|
||||
<EllipsisVerticalIcon size="xl" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
className="flex gap-3"
|
||||
onClick={() =>
|
||||
onOpenClosePilotForm(PilotFormMode.EDIT, pilotId)
|
||||
}
|
||||
>
|
||||
<PenIcon size="lg" />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="flex gap-3"
|
||||
onClick={() =>
|
||||
onOpenClosePilotForm(PilotFormMode.VIEW, pilotId)
|
||||
}
|
||||
>
|
||||
<EyeIcon size="lg" />
|
||||
View
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem className="flex gap-3">
|
||||
<TrashIcon size="lg" />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
enableSorting: false
|
||||
cell: (info) => <ActionMenu pilotId={info.row.original.rowKey} />
|
||||
}
|
||||
];
|
||||
|
||||
@@ -131,31 +144,32 @@ const Pilots: React.FC<unknown> = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 w-full rounded-xl py-4 px-8 shadow-md backdrop-saturate-200 backdrop-blur-2xl bg-opacity-80 border border-white/80 bg-white mt-6">
|
||||
<div className="col-span-1">
|
||||
<Typography variant="h2">Pilots</Typography>
|
||||
</div>
|
||||
<div className="col-span-1 justify-self-end">
|
||||
<Box sx={{ margin: '20px' }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<Typography variant="h4">Pilots</Typography>
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="right" size={1}>
|
||||
<Button
|
||||
className="flex justify-center gap-3"
|
||||
variant="filled"
|
||||
onClick={() => onOpenClosePilotForm(PilotFormMode.ADD)}
|
||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
data-testid="pilot-add-button"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Pilot
|
||||
</Button>
|
||||
</div>
|
||||
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />}
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
{pilots.length > 0 && <Table columns={columns} data={pilots} />}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<PilotForm
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
mode={pilotFormMode}
|
||||
onOpenClose={(mode) => onOpenClosePilotForm(mode)}
|
||||
pilotId={selectedPilotId}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ISiteNavProps } from './ISiteNavProps';
|
||||
// import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarLinks,
|
||||
NavbarMenu,
|
||||
NavbarItemProps,
|
||||
PlaneIcon,
|
||||
SignOutIcon,
|
||||
Spinner,
|
||||
@@ -37,10 +32,15 @@ const SiteNav: React.FC<unknown> = () => {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const { inProgress, instance } = useMsal();
|
||||
const navItems: NavbarItemProps[] = [
|
||||
const navigate = useNavigate();
|
||||
const pages = [
|
||||
{
|
||||
name: 'Logbook',
|
||||
url: '/'
|
||||
},
|
||||
{
|
||||
name: 'Pilots',
|
||||
url: '#'
|
||||
url: '/pilots'
|
||||
}
|
||||
];
|
||||
const handleSignIn = async () => {
|
||||
@@ -83,6 +83,20 @@ const SiteNav: React.FC<unknown> = () => {
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
const handlePageClick = (url: string) => {
|
||||
navigate(url);
|
||||
};
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<SignOutIcon />
|
||||
<Typography sx={{ marginLeft: '10px', textAlign: 'center' }}>
|
||||
Sign Out
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const callback = instance.addEventCallback(
|
||||
@@ -150,107 +164,16 @@ const SiteNav: React.FC<unknown> = () => {
|
||||
}, [isAuthenticated]);
|
||||
|
||||
return (
|
||||
<Navbar className="mt-6">
|
||||
<NavbarBrand>
|
||||
<img height={40} width={40} src="noahspan-logo.png" />{' '}
|
||||
<PlaneIcon size="2x" />
|
||||
</NavbarBrand>
|
||||
<NavbarLinks items={navItems} />
|
||||
<NavbarMenu>
|
||||
<div className="flex items-center gap-2 hidden lg:inline-block">
|
||||
{!loading && isAuthenticated && (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<div>
|
||||
{userPhoto && (
|
||||
<img
|
||||
className="rounded-full cursor-pointer"
|
||||
height="40"
|
||||
width="40"
|
||||
src={userPhoto}
|
||||
<Navbar
|
||||
handlePageClick={handlePageClick}
|
||||
handleSignIn={handleSignIn}
|
||||
isAuthenticated={isAuthenticated}
|
||||
logo={<PlaneIcon size="2x" />}
|
||||
pages={pages}
|
||||
settings={<Settings />}
|
||||
userPhoto={userPhoto}
|
||||
/>
|
||||
)}
|
||||
{!userPhoto && (
|
||||
<div className="rounded-full cursor-pointer text-white text-center pt-2 bg-black h-[40px] w-[40px]">
|
||||
NS
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<Typography
|
||||
className="flex justify-center gap-3"
|
||||
variant="small"
|
||||
>
|
||||
<SignOutIcon size="lg" />
|
||||
Sign Out
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
{loading && isAuthenticated && (
|
||||
<div className="flex justify-center gap-3">
|
||||
<Spinner size="xs" />
|
||||
<Typography variant="small">Loading...</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!isAuthenticated && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
onClick={handleSignIn}
|
||||
loading={inProgress === InteractionStatus.Login ? true : false}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* <IconButton
|
||||
variant='text'
|
||||
className='ml-auto h-6 w-6 text-inherit hover:bg-transparent focus:bg-transparent active:bg-transparent lg:hidden'
|
||||
ripple={false}
|
||||
onClick={() => setOpenNav(!openNav)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBars} size='2x' />
|
||||
</IconButton> */}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Navbar
|
||||
// classNames={{
|
||||
// base: 'bg-transparent z-0'
|
||||
// }}
|
||||
// isBlurred={false}
|
||||
// maxWidth="full"
|
||||
// data-testid="flying-navbar"
|
||||
// >
|
||||
// <NavbarBrand>
|
||||
// <Logo className="pr-3" height={50} width={50} data-testid="logo" />
|
||||
// <Plane size="2xl" />
|
||||
// </NavbarBrand>
|
||||
// <NavbarContent justify="center">
|
||||
// <NavbarItem>
|
||||
// {appContext.state.featureFlags.find(
|
||||
// (featureFlag) => featureFlag.key === 'flying-pilots'
|
||||
// )?.enabled && (
|
||||
// <Link>
|
||||
// <ReactRouterLink to="/">Pilots</ReactRouterLink>
|
||||
// </Link>
|
||||
// )}
|
||||
// </NavbarItem>
|
||||
// </NavbarContent>
|
||||
// <NavbarContent justify='end'>
|
||||
// <Login
|
||||
// loginCompleted={loginCompleted}
|
||||
// loginView='compact'
|
||||
// />
|
||||
// </NavbarContent>
|
||||
// </Navbar>
|
||||
// );
|
||||
};
|
||||
|
||||
export default SiteNav;
|
||||
|
||||
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 {
|
||||
body {
|
||||
@apply bg-[#ECEFF1];
|
||||
@apply bg-[#fafaf9];
|
||||
@apply text-black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import '@noahspan/noahspan-components/noahspan-components.css';
|
||||
import { PublicClientApplication } from '@azure/msal-browser';
|
||||
import { MsalProvider } from '@azure/msal-react';
|
||||
|
||||
const pca: PublicClientApplication = new PublicClientApplication({
|
||||
const msalInstance: PublicClientApplication = new PublicClientApplication({
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
|
||||
@@ -16,9 +16,10 @@ const pca: PublicClientApplication = new PublicClientApplication({
|
||||
}
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
msalInstance.initialize().then(() => {
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<MsalProvider instance={pca}>
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
@@ -26,4 +27,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</AppContextProvider>
|
||||
</MsalProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
27
compose.yaml
Normal file
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