switching to sqlite
15
.github/workflows/api_build.yaml
vendored
@@ -20,12 +20,6 @@ jobs:
|
|||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
api
|
api
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
name: Install pnpm
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Install Nest CLI
|
- name: Install Nest CLI
|
||||||
run: |
|
run: |
|
||||||
npm install -g @nestjs/cli
|
npm install -g @nestjs/cli
|
||||||
@@ -37,16 +31,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
npm ci
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
pnpm --filter api build
|
npm run build
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
run: |
|
|
||||||
pnpm --filter api --prod deploy ./.prod/api
|
|
||||||
|
|
||||||
- name: Log into Docker Hub
|
- name: Log into Docker Hub
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|||||||
44
.github/workflows/main.yaml
vendored
@@ -19,17 +19,17 @@ jobs:
|
|||||||
version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
# deploy-api:
|
deploy-api:
|
||||||
# name: deploy-api
|
name: deploy-api
|
||||||
# needs:
|
needs:
|
||||||
# - changes
|
- changes
|
||||||
# - build-api
|
- build-api
|
||||||
# uses: ./.github/workflows/deploy.yaml
|
uses: ./.github/workflows/deploy.yaml
|
||||||
# with:
|
with:
|
||||||
# app_name: flying-api
|
app_name: flying-api
|
||||||
# environment_name: test
|
environment_name: test
|
||||||
# version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
# secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-app:
|
build-app:
|
||||||
if: ${{ needs.changes.outputs.app == 'true' }}
|
if: ${{ needs.changes.outputs.app == 'true' }}
|
||||||
@@ -42,14 +42,14 @@ jobs:
|
|||||||
version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
# deploy-app:
|
deploy-app:
|
||||||
# name: deploy-app
|
name: deploy-app
|
||||||
# needs:
|
needs:
|
||||||
# - changes
|
- changes
|
||||||
# - build-app
|
- build-app
|
||||||
# uses: ./.github/workflows/deploy.yaml
|
uses: ./.github/workflows/deploy.yaml
|
||||||
# with:
|
with:
|
||||||
# app_name: flying-app
|
app_name: flying-app
|
||||||
# environment_name: test
|
environment_name: test
|
||||||
# version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
# secrets: inherit
|
secrets: inherit
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -5,5 +5,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.dapr
|
.dapr
|
||||||
**/**/secrets.json
|
**/**/secrets.json
|
||||||
.prod
|
.prod
|
||||||
database
|
|
||||||
0
.turbo/daemon/a55a0ce1e1048a13-turbo.log.2025-09-14
Normal file
23
Dockerfile
@@ -22,24 +22,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM node:22-slim AS base
|
FROM --platform=linux/amd64 node:22-slim AS base
|
||||||
|
|
||||||
FROM base AS migrate
|
|
||||||
WORKDIR migrations
|
|
||||||
COPY ./migrations .
|
|
||||||
RUN npm i -g pnpm
|
|
||||||
RUN pnpm install
|
|
||||||
|
|
||||||
FROM base AS api
|
FROM base AS api
|
||||||
|
COPY ./api/entrypoint.sh ./entrypoint.sh
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
WORKDIR api
|
WORKDIR api
|
||||||
COPY ./.prod/api .
|
COPY ./api/dist ./dist
|
||||||
|
COPY ./api/package.json package-lock.json .
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["node", "dist/main.js"]
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||||
|
|
||||||
FROM base AS app
|
FROM base AS app
|
||||||
WORKDIR /app
|
WORKDIR app
|
||||||
COPY ./.prod/app .
|
COPY ./app/dist ./dist
|
||||||
RUN npm i -g serve
|
RUN npm i -g serve
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD [ "serve", "-s", "dist", "-p", "8080" ]
|
CMD [ "serve", "-s", "dist", "-p", "8080" ]
|
||||||
3
api/.gitignore
vendored
@@ -57,3 +57,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|||||||
|
|
||||||
|
|
||||||
local.settings.json
|
local.settings.json
|
||||||
|
|
||||||
|
|
||||||
|
/src/database/*.db
|
||||||
|
|||||||
3
api/entrypoint.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
npx typeorm migration:run -d ./dist/config/typeorm-cli.config.js
|
||||||
|
node ./dist/main.js
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"typeorm": "npm run build && npx typeorm -d dist/config/typeorm-cli.config.js",
|
"typeorm": "npm run build && npx typeorm -d dist/database/data-source.js",
|
||||||
"migration:generate": "npm run typeorm -- migration:generate",
|
"migration:generate": "npm run typeorm -- migration:generate",
|
||||||
"migration:run": "npm run typeorm -- migration:run",
|
"migration:run": "npm run typeorm -- migration:run",
|
||||||
"migration:revert": "npm run typeorm -- migration:revert"
|
"migration:revert": "npm run typeorm -- migration:revert"
|
||||||
@@ -25,21 +25,29 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/storage-blob": "^12.27.0",
|
"@azure/storage-blob": "^12.27.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@nestjs/axios": "^3.0.3",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^11.1.6",
|
||||||
"@nestjs/config": "^3.2.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^11.1.6",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/jwt": "^11.0.0",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.1.6",
|
||||||
|
"@nestjs/serve-static": "^5.0.3",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"@noahspan/azure-database": "^3.1.2",
|
"@noahspan/azure-database": "^3.1.2",
|
||||||
"@noahspan/noahspan-modules": "^1.1.5",
|
"@noahspan/noahspan-modules": "^1.2.8",
|
||||||
"@schematics/angular": "^17.3.7",
|
"@schematics/angular": "^17.3.7",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
|
"better-sqlite3": "^12.2.0",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
|
"express-session": "^1.18.2",
|
||||||
|
"jwks-rsa": "^3.2.0",
|
||||||
|
"node-gyp": "^11.4.1",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-openidconnect": "^0.1.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sqlite3": "^5.1.7",
|
|
||||||
"typeorm": "^0.3.25",
|
"typeorm": "^0.3.25",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"uuidv4": "^6.2.13"
|
"uuidv4": "^6.2.13"
|
||||||
@@ -48,11 +56,13 @@
|
|||||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^11.1.6",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/passport-azure-ad": "^4.3.6",
|
"@types/passport-azure-ad": "^4.3.6",
|
||||||
|
"@types/passport-openidconnect": "^0.1.3",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AppController } from './app.controller';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
describe('AppController', () => {
|
|
||||||
let appController: AppController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService]
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('root', () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { FeatureFlagModule } from './featureFlag/feature-flag.module'
|
import { HealthModule } from './health/health.module';
|
||||||
import { LogModule } from './log/log.module';
|
import { LogModule } from './log/log.module';
|
||||||
import { PilotModule } from './pilot/pilot.module';
|
import { PilotModule } from './pilot/pilot.module';
|
||||||
import { APP_FILTER, APP_GUARD, Reflector } from '@nestjs/core';
|
import { APP_FILTER, APP_GUARD, Reflector } from '@nestjs/core';
|
||||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { AuthGuard, AuthModule, UserModule } from '@noahspan/noahspan-modules';
|
import { AuthModule, MsGraphModule } from '@noahspan/noahspan-modules';
|
||||||
import configuration from './config/configuration';
|
import configuration from './config/configuration';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { dataSourceOptions } from './config/typeorm-cli.config';
|
import { dataSourceOptions } from './database/data-source';
|
||||||
import { TrackModule } from './track/track.module';
|
import { TrackModule } from './track/track.module';
|
||||||
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -18,9 +20,9 @@ import { TrackModule } from './track/track.module';
|
|||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
return {
|
return {
|
||||||
clientId: configService.get<string>('clientId'),
|
audience: configService.get<string>('audience'),
|
||||||
clientSecret: configService.get<string>('clientSecret'),
|
issuerUrl: configService.get<string>('issuer'),
|
||||||
tenantId: configService.get<string>('tenantId')
|
jwksUri: configService.get<string>('jwksUri')
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -28,12 +30,15 @@ import { TrackModule } from './track/track.module';
|
|||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [configuration]
|
load: [configuration]
|
||||||
}),
|
}),
|
||||||
FeatureFlagModule,
|
// HealthModule,
|
||||||
LogModule,
|
// LogModule,
|
||||||
PilotModule,
|
PilotModule,
|
||||||
TrackModule,
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: join(__dirname, '../..', 'client', 'dist')
|
||||||
|
}),
|
||||||
|
// TrackModule,
|
||||||
TypeOrmModule.forRoot(dataSourceOptions),
|
TypeOrmModule.forRoot(dataSourceOptions),
|
||||||
UserModule.registerAsync({
|
MsGraphModule.registerAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
@@ -49,7 +54,7 @@ import { TrackModule } from './track/track.module';
|
|||||||
{
|
{
|
||||||
provide: APP_FILTER,
|
provide: APP_FILTER,
|
||||||
useClass: HttpExceptionFilter
|
useClass: HttpExceptionFilter
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface AuthModuleOptions {
|
|
||||||
tenantId: string;
|
|
||||||
clientId: string;
|
|
||||||
clientSecret: string;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
|
||||||
import { AuthModuleOptions } from './auth.interface';
|
|
||||||
|
|
||||||
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<AuthModuleOptions>().build()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PassportModule } from '@nestjs/passport';
|
|
||||||
import { AzureAdStrategy } from './auth.strategy';
|
|
||||||
import { ConfigurableModuleClass } from './auth.module-definition';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
PassportModule.register({
|
|
||||||
defaultStrategy: 'azure-ad'
|
|
||||||
})
|
|
||||||
],
|
|
||||||
providers: [AzureAdStrategy]
|
|
||||||
})
|
|
||||||
export class AuthModule extends ConfigurableModuleClass {}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Inject, Injectable } from "@nestjs/common";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import { AuthModuleOptions } from './auth.interface'
|
|
||||||
import { MODULE_OPTIONS_TOKEN } from "./auth.module-definition";
|
|
||||||
import { BearerStrategy } from 'passport-azure-ad'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AzureAdStrategy extends PassportStrategy(
|
|
||||||
BearerStrategy,
|
|
||||||
'azure-ad'
|
|
||||||
) {
|
|
||||||
constructor(@Inject(MODULE_OPTIONS_TOKEN) authModuleOptions: AuthModuleOptions) {
|
|
||||||
super({
|
|
||||||
identityMetadata: `https://login.microsoftonline.com/${authModuleOptions.tenantId}/.well-known/openid-configuration`,
|
|
||||||
clientID: authModuleOptions.clientId,
|
|
||||||
audience: `api://${authModuleOptions.clientId}`,
|
|
||||||
loggingLevel: 'info',
|
|
||||||
loggingNoPII: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(data: any): Promise<any> {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||||
|
audience: process.env.AUDIENCE,
|
||||||
clientId: process.env.CLIENT_ID,
|
clientId: process.env.CLIENT_ID,
|
||||||
clientSecret: process.env.CLIENT_SECRET,
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
issuer: process.env.ISSUER_URL,
|
||||||
|
jwksUri: process.env.JWKS_URI,
|
||||||
tenantId: process.env.TENANT_ID
|
tenantId: process.env.TENANT_ID
|
||||||
})
|
})
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import configuration from './configuration';
|
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
const configService = new ConfigService();
|
const configService = new ConfigService();
|
||||||
|
|
||||||
export const dataSourceOptions: DataSourceOptions = {
|
export const dataSourceOptions: DataSourceOptions = {
|
||||||
type: 'sqlite',
|
type: 'better-sqlite3',
|
||||||
database: configService.get<string>('DB_PATH'),
|
database: configService.get<string>('DB_PATH'),
|
||||||
entities: ['dist/**/*.entity.js'],
|
entities: ['dist/**/*.entity.js'],
|
||||||
migrations: ['dist/migrations/*.js'],
|
migrations: ['dist/database/migrations/*.js'],
|
||||||
synchronize: configService.get<boolean>('DB_SYNC')
|
synchronize: configService.get<boolean>('DB_SYNC')
|
||||||
}
|
}
|
||||||
|
|
||||||
4
api/src/database/litestream.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dbs:
|
||||||
|
- path: /var/lib/data/flying.db
|
||||||
|
replicas:
|
||||||
|
- path: /mnt/data/backup
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
export class InitialMigration1754234179211 implements MigrationInterface {
|
export class InitialMigration1758802917932 implements MigrationInterface {
|
||||||
name = 'InitialMigration1754234179211'
|
name = 'InitialMigration1758802917932'
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`CREATE TABLE "certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
await queryRunner.query(`CREATE TABLE "certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||||
await queryRunner.query(`CREATE TABLE "endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
await queryRunner.query(`CREATE TABLE "endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||||
await queryRunner.query(`CREATE TABLE "medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar)`);
|
await queryRunner.query(`CREATE TABLE "medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||||
await queryRunner.query(`CREATE TABLE "pilots" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "address" varchar NOT NULL, "city" varchar NOT NULL, "state" varchar NOT NULL, "postalCode" varchar NOT NULL, "email" varchar NOT NULL, "phone" varchar NOT NULL)`);
|
await queryRunner.query(`CREATE TABLE "pilots" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "address" varchar NOT NULL, "city" varchar NOT NULL, "state" varchar NOT NULL, "postalCode" varchar NOT NULL, "email" varchar NOT NULL, "phone" varchar NOT NULL, "userId" varchar NOT NULL)`);
|
||||||
await queryRunner.query(`CREATE TABLE "logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar)`);
|
await queryRunner.query(`CREATE TABLE "logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar)`);
|
||||||
await queryRunner.query(`CREATE TABLE "tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar)`);
|
await queryRunner.query(`CREATE TABLE "tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar)`);
|
||||||
await queryRunner.query(`CREATE TABLE "temporary_certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_05a68997dc2d27dfcc642a4cf51" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
await queryRunner.query(`CREATE TABLE "temporary_certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_05a68997dc2d27dfcc642a4cf51" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
HttpException,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { FeatureFlagService } from './feature-flag.service';
|
|
||||||
import { CustomError } from '../error/customError';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
|
|
||||||
@Controller('featureFlags')
|
|
||||||
@UseGuards(AuthGuard('azure-ad'))
|
|
||||||
export class FeatureFlagController {
|
|
||||||
constructor(private readonly featureFlagService: FeatureFlagService) {}
|
|
||||||
|
|
||||||
@Get(':partitionKey/:rowKey')
|
|
||||||
async find(
|
|
||||||
@Param('partitionKey') partitionKey: string,
|
|
||||||
@Param('rowKey') rowKey: string
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
return await this.featureFlagService.find(partitionKey, rowKey);
|
|
||||||
} catch (error) {
|
|
||||||
const customError = error as CustomError;
|
|
||||||
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
async findAll() {
|
|
||||||
try {
|
|
||||||
return await this.featureFlagService.findAll();
|
|
||||||
} catch (error) {
|
|
||||||
const customError = error as CustomError;
|
|
||||||
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export class FeatureFlagDto {
|
|
||||||
partitionKey: string;
|
|
||||||
rowKey: string;
|
|
||||||
active: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { EntityString } from '@noahspan/azure-database';
|
|
||||||
|
|
||||||
export class FeatureFlag {
|
|
||||||
@EntityString() partitionKey: string;
|
|
||||||
@EntityString() rowKey: string;
|
|
||||||
@EntityString() active: string;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { FeatureFlagController } from './feature-flag.controller';
|
|
||||||
import { FeatureFlagService } from './feature-flag.service';
|
|
||||||
import { AzureTableStorageModule } from '@noahspan/azure-database';
|
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { FeatureFlag } from './feature-flag.entity';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
AzureTableStorageModule.forRootAsync({
|
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: async (configService: ConfigService) => {
|
|
||||||
return {
|
|
||||||
connectionString: configService.get<string>('azureStorageConnectionString')
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject: [ConfigService]
|
|
||||||
}),
|
|
||||||
AzureTableStorageModule.forFeature(FeatureFlag, {
|
|
||||||
createTableIfNotExists: false,
|
|
||||||
table: 'featureFlags'
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [FeatureFlagController],
|
|
||||||
providers: [FeatureFlagService]
|
|
||||||
})
|
|
||||||
export class FeatureFlagModule {}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository, Repository } from '@noahspan/azure-database';
|
|
||||||
import { FeatureFlag } from './feature-flag.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FeatureFlagService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(FeatureFlag) private readonly featureFlagRepository: Repository<FeatureFlag>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async find(partitionKey: string, rowKey: string): Promise<FeatureFlag> {
|
|
||||||
return await this.featureFlagRepository.find(partitionKey, rowKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(): Promise<FeatureFlag[]> {
|
|
||||||
return await this.featureFlagRepository.findAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
api/src/health/health.controller.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HealthController } from './health.controller';
|
||||||
|
import { HealthService } from './health.service';
|
||||||
|
|
||||||
|
describe('HealthController', () => {
|
||||||
|
let controller; HealthController;
|
||||||
|
|
||||||
|
const mockHealthService = {
|
||||||
|
isDatabaseConnected: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [HealthController],
|
||||||
|
providers: [HealthService]
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<HealthController>(HealthController);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isHealthy => should return true', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return database connected', async () => {
|
||||||
|
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(true);
|
||||||
|
|
||||||
|
const result = await controller.isHealthy();
|
||||||
|
|
||||||
|
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isHealthy => should return error', async () => {
|
||||||
|
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = await controller.isHealthy();
|
||||||
|
|
||||||
|
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
})
|
||||||
|
})
|
||||||
27
api/src/health/health.controller.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { HealthService } from './health.service';
|
||||||
|
import { CustomError } from 'src/error/customError';
|
||||||
|
|
||||||
|
|
||||||
|
@Controller('health')
|
||||||
|
export class HealthController {
|
||||||
|
constructor(
|
||||||
|
private readonly healthService: HealthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async isHealthy(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return await this.healthService.isDatabaseConnected();
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
api/src/health/health.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HealthController } from './health.controller';
|
||||||
|
import { HealthService } from './health.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [HealthController],
|
||||||
|
providers: [
|
||||||
|
HealthService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class HealthModule {}
|
||||||
14
api/src/health/health.service.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HealthService } from './health.service';
|
||||||
|
|
||||||
|
describe('HealthService', () => {
|
||||||
|
let service: HealthService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [HealthService]
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<HealthService>(HealthService);
|
||||||
|
});
|
||||||
|
});
|
||||||
22
api/src/health/health.service.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CustomError } from '../error/customError';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HealthService {
|
||||||
|
constructor(private dataSource: DataSource) {}
|
||||||
|
|
||||||
|
async isDatabaseConnected(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
|
||||||
|
|
||||||
|
if (isDatabaseConnected) {
|
||||||
|
return isDatabaseConnected;
|
||||||
|
} else {
|
||||||
|
throw new CustomError('Database not connected', 'Database not connected', 400)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,3 @@
|
|||||||
// export class Log {
|
|
||||||
// partitionKey: string;
|
|
||||||
// rowKey: 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;
|
|
||||||
// tracks?: string[];
|
|
||||||
// notes?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||||
import { TrackEntity } from 'src/track/track.entity';
|
import { TrackEntity } from 'src/track/track.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||||
|
|||||||
@@ -3,15 +3,26 @@ import { AppModule } from './app.module';
|
|||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import * as session from 'express-session';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const httpService = new HttpService();
|
const httpService = new HttpService();
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.enableCors();
|
app.enableCors({
|
||||||
|
origin: 'http://localhost:8080', // Allow requests from your frontend's origin
|
||||||
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
|
credentials: true, // If you need to send cookies or authorization headers
|
||||||
|
});
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: 'blah',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
httpService.axiosRef.interceptors.response.use(
|
httpService.axiosRef.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { PilotDto } from './pilot.dto';
|
|||||||
import { PilotEntity } from './pilot.entity';
|
import { PilotEntity } from './pilot.entity';
|
||||||
import { PilotService } from './pilot.service';
|
import { PilotService } from './pilot.service';
|
||||||
import { CustomError } from '../error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
import { AuthGuard } from '@noahspan/noahspan-modules'
|
|
||||||
import { PilotInterceptor } from './interceptors/pilot.interceptor';
|
import { PilotInterceptor } from './interceptors/pilot.interceptor';
|
||||||
|
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||||
|
|
||||||
@Controller('pilots')
|
@Controller('pilots')
|
||||||
// @UseInterceptors(new PilotInterceptor())
|
// @UseInterceptors(new PilotInterceptor())
|
||||||
@@ -34,6 +34,7 @@ export class PilotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
async findAll() {
|
async findAll() {
|
||||||
try {
|
try {
|
||||||
return await this.pilotService.findAll();
|
return await this.pilotService.findAll();
|
||||||
@@ -44,7 +45,7 @@ export class PilotController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() pilotDto: PilotDto) {
|
async create(@Body() pilotDto: PilotDto) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,22 +1,3 @@
|
|||||||
// import { EntityString } from '@noahspan/azure-database';
|
|
||||||
|
|
||||||
// export class Pilot {
|
|
||||||
// @EntityString() partitionKey: string;
|
|
||||||
// @EntityString() rowKey: string;
|
|
||||||
// @EntityString() id: string;
|
|
||||||
// @EntityString() name: string;
|
|
||||||
// @EntityString() address?: string;
|
|
||||||
// @EntityString() city?: string;
|
|
||||||
// @EntityString() state?: string;
|
|
||||||
// @EntityString() postalCode?: string;
|
|
||||||
// @EntityString() email?: string;
|
|
||||||
// @EntityString() phone?: string;
|
|
||||||
// @EntityString() medicalClass?: string;
|
|
||||||
// @EntityString() medicalExpiration: string;
|
|
||||||
// @EntityString() certificates: string;
|
|
||||||
// @EntityString() endorsements: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
import { LogEntity } from 'src/log/log.entity';
|
import { LogEntity } from 'src/log/log.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||||
import { CertificateEntity } from '../certificate/certificate.entity';
|
import { CertificateEntity } from '../certificate/certificate.entity';
|
||||||
@@ -49,6 +30,9 @@ export class PilotEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: string | null;
|
||||||
|
|
||||||
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
|
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
|
||||||
logs: LogEntity[];
|
logs: LogEntity[];
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Licensed under the MIT License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { LogLevel } from '@azure/msal-browser';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration object to be passed to MSAL instance on creation.
|
|
||||||
* For a full list of MSAL.js configuration parameters, visit:
|
|
||||||
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const msalConfig = {
|
|
||||||
auth: {
|
|
||||||
clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply.
|
|
||||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain
|
|
||||||
redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration.
|
|
||||||
postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
|
|
||||||
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
|
|
||||||
},
|
|
||||||
cache: {
|
|
||||||
cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
|
|
||||||
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
|
|
||||||
},
|
|
||||||
system: {
|
|
||||||
loggerOptions: {
|
|
||||||
loggerCallback: (level: any, message: any, containsPii: any) => {
|
|
||||||
if (containsPii) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (level) {
|
|
||||||
case LogLevel.Error:
|
|
||||||
console.error(message);
|
|
||||||
return;
|
|
||||||
case LogLevel.Info:
|
|
||||||
console.info(message);
|
|
||||||
return;
|
|
||||||
case LogLevel.Verbose:
|
|
||||||
console.debug(message);
|
|
||||||
return;
|
|
||||||
case LogLevel.Warning:
|
|
||||||
console.warn(message);
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scopes you add here will be prompted for user consent during sign-in.
|
|
||||||
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
|
|
||||||
* For more information about OIDC scopes, visit:
|
|
||||||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
|
|
||||||
*/
|
|
||||||
export const loginRequest = {
|
|
||||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional silentRequest object can be used to achieve silent SSO
|
|
||||||
* between applications by providing a "login_hint" property.
|
|
||||||
*/
|
|
||||||
// export const silentRequest = {
|
|
||||||
// scopes: ["openid", "profile"],
|
|
||||||
// loginHint: "example@domain.net"
|
|
||||||
// };
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { IActionMenuProps } from './IActionMenuProps';
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
Icon,
|
|
||||||
IconName,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Menu,
|
|
||||||
MenuItem
|
|
||||||
} from '@noahspan/noahspan-components';
|
|
||||||
import { FormMode } from '../../enums/formMode';
|
|
||||||
import { useIsAuthenticated } from '@azure/msal-react';
|
|
||||||
|
|
||||||
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
|
||||||
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const isAuthenticated = useIsAuthenticated();
|
|
||||||
|
|
||||||
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorElAction(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCloseActionMenu = () => {
|
|
||||||
setAnchorElAction(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<IconButton onClick={onOpenActionMenu}>
|
|
||||||
<Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorElAction}
|
|
||||||
keepMounted
|
|
||||||
open={Boolean(anchorElAction)}
|
|
||||||
onClose={onCloseActionMenu}
|
|
||||||
>
|
|
||||||
{isAuthenticated &&
|
|
||||||
<>
|
|
||||||
<MenuItem onClick={() => onOpenCloseForm(FormMode.EDIT, id)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Icon iconName={IconName.PEN} size="lg" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Edit</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
{onOpenCloseTracks &&
|
|
||||||
<MenuItem onClick={() => onOpenCloseTracks!(FormMode.EDIT, id)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Icon iconName={IconName.MAP_LOCATION_DOT} size="lg" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Tracks</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
|
|
||||||
}
|
|
||||||
<MenuItem onClick={() => onOpenCloseForm(FormMode.VIEW, id)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Icon iconName={IconName.EYE} size="lg" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>View</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
{isAuthenticated &&
|
|
||||||
<>
|
|
||||||
<hr className="my-3" />
|
|
||||||
<MenuItem onClick={() => onDelete(id)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Icon iconName={IconName.TRASH} size="lg" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Delete</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionMenu;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { Box, Card, CardContent, Container, Grid, Skeleton, Spinner, Stack, theme, Typography, useMediaQuery } from "@noahspan/noahspan-components";
|
|
||||||
import LogbookCard from "../logbookCard/LogbookCard";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useLogs } from "../../hooks/logs/UseLogs";
|
|
||||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
|
||||||
|
|
||||||
const Flights = () => {
|
|
||||||
const [flights, setFlights] = useState<ILogbookEntry[]>([]);
|
|
||||||
const { logs, isLoading } = useLogs();
|
|
||||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(logs)
|
|
||||||
const flights: ILogbookEntry[] | undefined = logs?.filter((log: ILogbookEntry) => {
|
|
||||||
if (log.tracks && log.tracks.length > 0) {
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (flights && flights.length > 0) {
|
|
||||||
setFlights(flights)
|
|
||||||
}
|
|
||||||
}, [logs])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Box sx={{ margin: '20px' }}>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid size={isMedium ? 11 : 6}>
|
|
||||||
<Typography variant="h4">Flights</Typography>
|
|
||||||
</Grid>
|
|
||||||
{!isLoading &&
|
|
||||||
<Grid size={12}>
|
|
||||||
<LogbookCard logs={flights} mode='flights' />
|
|
||||||
</Grid>
|
|
||||||
}
|
|
||||||
{isLoading && [...Array(6)].map((_element, index) => {
|
|
||||||
return (
|
|
||||||
<Grid display='flex' justifyContent='center' size={12}>
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
sx={{
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardContent>
|
|
||||||
<Grid container>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Skeleton height={60} width={200} />
|
|
||||||
<Skeleton height={30} width={200} />
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Skeleton height={300} />
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
{[...Array(4)].map((_element, index) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Skeleton height={20} width={300} />
|
|
||||||
<Skeleton height={40} width={300} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Flights;
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Grid, Typography } from "@noahspan/noahspan-components";
|
|
||||||
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
|
||||||
import ActionMenu from "../actionMenu/ActionMenu";
|
|
||||||
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
|
||||||
|
|
||||||
|
|
||||||
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
|
||||||
return (
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{logs.map((log) => {
|
|
||||||
const date = new Date(log.date);
|
|
||||||
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid size={12}>
|
|
||||||
<Card key={log.id}>
|
|
||||||
<CardHeader
|
|
||||||
action={mode === 'logbook' ? <ActionMenu id={log.id} onDelete={onDelete!} onOpenCloseForm={onOpenCloseForm!} /> : null}
|
|
||||||
title={formattedDate}
|
|
||||||
slotProps={{
|
|
||||||
subheader: {
|
|
||||||
fontSize: '16px'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: '24px',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Grid container spacing={1}>
|
|
||||||
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
|
||||||
<Grid size={12}>
|
|
||||||
<LogTrackMaps
|
|
||||||
logId={log.id}
|
|
||||||
tracks={log.tracks}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
}
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="subtitle2">Aircraft Make and Model</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="body1">{log.aircraftMakeModel}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="subtitle2">Route From</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="body1">{log.routeFrom}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="subtitle2">Route To</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="body1">{log.routeTo}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="subtitle2">Duration Of Flight</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="body1">{log.durationOfFlight}</Typography>
|
|
||||||
</Grid>
|
|
||||||
{mode === 'logbook' && log.tracks && log.tracks.length > 0 &&
|
|
||||||
<>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="subtitle2">Tracks</Typography>
|
|
||||||
</Grid>
|
|
||||||
{log.tracks.map((track: { id: string; order: number; url: string}) => {
|
|
||||||
const trackSplit = track.url.split('/')
|
|
||||||
const filename = trackSplit[trackSplit.length - 1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="body1">{filename}</Typography>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{log.notes &&
|
|
||||||
<>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="subtitle2">Notes</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<Typography variant="body1">{log.notes}</Typography>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LogbookCard;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Grid, Typography } from "@noahspan/noahspan-components"
|
|
||||||
import { PilotCardProps } from "./PilotCardProps.interface"
|
|
||||||
import ActionMenu from "../actionMenu/ActionMenu"
|
|
||||||
|
|
||||||
const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
|
||||||
return (
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{pilots.map((pilot) => {
|
|
||||||
return (
|
|
||||||
<Grid size={12}>
|
|
||||||
<Card key={pilot.rowKey}>
|
|
||||||
<CardHeader
|
|
||||||
action={<ActionMenu id={pilot.rowKey} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />}
|
|
||||||
title={pilot.name}
|
|
||||||
slotProps={{
|
|
||||||
title: {
|
|
||||||
fontSize: '24px',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PilotCard;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import {
|
|
||||||
AuthenticationResult,
|
|
||||||
InteractionRequiredAuthError
|
|
||||||
} from '@azure/msal-browser';
|
|
||||||
import { useMsal } from '@azure/msal-react';
|
|
||||||
|
|
||||||
export const useAccessToken = () => {
|
|
||||||
const { accounts, instance } = useMsal();
|
|
||||||
const getAccessToken = async () => {
|
|
||||||
const tokenRequest = {
|
|
||||||
account: accounts[0],
|
|
||||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response: AuthenticationResult =
|
|
||||||
await instance.acquireTokenSilent(tokenRequest);
|
|
||||||
|
|
||||||
return `Bearer ${response.accessToken}`;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof InteractionRequiredAuthError) {
|
|
||||||
await instance.acquireTokenRedirect(tokenRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getAccessToken
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
body {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import App from './App.tsx';
|
|
||||||
import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { AuthenticationResult, EventMessage, EventType, PublicClientApplication } from '@azure/msal-browser';
|
|
||||||
import { MsalProvider } from '@azure/msal-react';
|
|
||||||
import { msalConfig } from './auth/msalConfig';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const msalInstance: PublicClientApplication = new PublicClientApplication(msalConfig);
|
|
||||||
|
|
||||||
msalInstance.initialize().then(() => {
|
|
||||||
const accounts = msalInstance.getAllAccounts();
|
|
||||||
|
|
||||||
if (accounts.length > 0) {
|
|
||||||
msalInstance.setActiveAccount(accounts[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
msalInstance.addEventCallback((event: EventMessage) => {
|
|
||||||
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
|
|
||||||
const payload = event.payload as AuthenticationResult;
|
|
||||||
const account = payload.account;
|
|
||||||
msalInstance.setActiveAccount(account);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<MsalProvider instance={msalInstance}>
|
|
||||||
<AppContextProvider>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</AppContextProvider>
|
|
||||||
</MsalProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
preview: {
|
|
||||||
port: 8080,
|
|
||||||
strictPort: true
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 8080,
|
|
||||||
strictPort: true,
|
|
||||||
host: '0.0.0.0'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
0
app/.gitignore → client/.gitignore
vendored
0
client/README.md
Normal file
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "app",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0-alpha",
|
"version": "2.0.0-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,25 +11,27 @@
|
|||||||
"serve": "serve -s dist"
|
"serve": "serve -s dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.0.1",
|
"@noahspan/noahspan-components": "^2.0.0-alpha-11",
|
||||||
"@azure/msal-react": "3.0.1",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@noahspan/noahspan-components": "^1.9.1",
|
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"react": "^18",
|
"oidc-spa": "^7.2.4",
|
||||||
"react-dom": "^18",
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-hook-form": "^7.51.4",
|
||||||
"react-leaflet": "^4",
|
"react-leaflet": "^4",
|
||||||
"react-leaflet-kml": "^2.1.2",
|
"react-leaflet-kml": "^2.1.2",
|
||||||
|
"react-oidc-context": "^3.3.0",
|
||||||
"react-router-dom": "^6.23.0",
|
"react-router-dom": "^6.23.0",
|
||||||
"swiper": "^11.2.6"
|
"swiper": "^11.2.6",
|
||||||
|
"tailwindcss": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@types/leaflet": "^1.9.16",
|
"@types/leaflet": "^1.9.16",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0"
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 618 B After Width: | Height: | Size: 618 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -3,17 +3,17 @@ import Flights from './components/flights/Flights';
|
|||||||
import Logbook from './components/logbook/Logbook';
|
import Logbook from './components/logbook/Logbook';
|
||||||
import Pilots from './components/pilots/Pilots';
|
import Pilots from './components/pilots/Pilots';
|
||||||
import SiteNav from './components/siteNav/SiteNav';
|
import SiteNav from './components/siteNav/SiteNav';
|
||||||
import { useIsAuthenticated } from '@azure/msal-react';
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const isAuthenticated = useIsAuthenticated()
|
const auth = useAuth();
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||||
return isAuthenticated ? children : <Navigate to='/' />
|
return auth.isAuthenticated ? children : <Navigate to='/' />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -23,7 +23,6 @@ const App = () => {
|
|||||||
<Route path='/' element={<Flights />} />
|
<Route path='/' element={<Flights />} />
|
||||||
<Route path="/logbook" element={<Logbook />} />
|
<Route path="/logbook" element={<Logbook />} />
|
||||||
<Route path="/pilots" element={<Pilots />} />
|
<Route path="/pilots" element={<Pilots />} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
8
client/src/auth/oidcConfig.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createReactOidc } from "oidc-spa/react";
|
||||||
|
|
||||||
|
export const { OidcProvider, useOidc, getOidc, withLoginEnforced, enforceLogin } = createReactOidc(async () => ({
|
||||||
|
issuerUri: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}/v2.0`,
|
||||||
|
clientId: import.meta.env.VITE_CLIENT_APP_ID,
|
||||||
|
homeUrl: import.meta.env.BASE_URL,
|
||||||
|
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`]
|
||||||
|
}));
|
||||||
78
client/src/components/actionMenu/ActionMenu.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { IActionMenuProps } from './IActionMenuProps';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Icon,
|
||||||
|
IconName
|
||||||
|
} from '@noahspan/noahspan-components';
|
||||||
|
import { FormMode } from '../../enums/formMode';
|
||||||
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
|
||||||
|
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
||||||
|
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const auth = useAuth();
|
||||||
|
|
||||||
|
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElAction(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCloseActionMenu = () => {
|
||||||
|
setAnchorElAction(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<></>
|
||||||
|
// <div>
|
||||||
|
// <IconButton onClick={onOpenActionMenu}>
|
||||||
|
// <Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
||||||
|
// </IconButton>
|
||||||
|
// <Menu
|
||||||
|
// anchorEl={anchorElAction}
|
||||||
|
// keepMounted
|
||||||
|
// open={Boolean(anchorElAction)}
|
||||||
|
// onClose={onCloseActionMenu}
|
||||||
|
// >
|
||||||
|
// {auth.isAuthenticated &&
|
||||||
|
// <>
|
||||||
|
// <MenuItem onClick={() => onOpenCloseForm(FormMode.EDIT, id)}>
|
||||||
|
// <ListItemIcon>
|
||||||
|
// <Icon iconName={IconName.PEN} size="lg" />
|
||||||
|
// </ListItemIcon>
|
||||||
|
// <ListItemText>Edit</ListItemText>
|
||||||
|
// </MenuItem>
|
||||||
|
// {onOpenCloseTracks &&
|
||||||
|
// <MenuItem onClick={() => onOpenCloseTracks!(FormMode.EDIT, id)}>
|
||||||
|
// <ListItemIcon>
|
||||||
|
// <Icon iconName={IconName.MAP_LOCATION_DOT} size="lg" />
|
||||||
|
// </ListItemIcon>
|
||||||
|
// <ListItemText>Tracks</ListItemText>
|
||||||
|
// </MenuItem>
|
||||||
|
// }
|
||||||
|
// </>
|
||||||
|
|
||||||
|
// }
|
||||||
|
// <MenuItem onClick={() => onOpenCloseForm(FormMode.VIEW, id)}>
|
||||||
|
// <ListItemIcon>
|
||||||
|
// <Icon iconName={IconName.EYE} size="lg" />
|
||||||
|
// </ListItemIcon>
|
||||||
|
// <ListItemText>View</ListItemText>
|
||||||
|
// </MenuItem>
|
||||||
|
// {auth.isAuthenticated &&
|
||||||
|
// <>
|
||||||
|
// <hr className="my-3" />
|
||||||
|
// <MenuItem onClick={() => onDelete(id)}>
|
||||||
|
// <ListItemIcon>
|
||||||
|
// <Icon iconName={IconName.TRASH} size="lg" />
|
||||||
|
// </ListItemIcon>
|
||||||
|
// <ListItemText>Delete</ListItemText>
|
||||||
|
// </MenuItem>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// </Menu>
|
||||||
|
// </div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionMenu;
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
|
||||||
DialogTitle,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconName,
|
IconName,
|
||||||
Spinner
|
Loading
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
|
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
|
||||||
|
|
||||||
@@ -22,25 +19,20 @@ const ConfirmationDialog = ({
|
|||||||
}: IDialogConfirmationProps) => {
|
}: IDialogConfirmationProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
onClose={onCancel}
|
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
sx={{
|
|
||||||
'& .MuiDialog-paper': { width: '2000px' }
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<h3>{title}</h3>
|
||||||
<DialogContent sx={{ textAlign: 'center' }}>
|
<DialogContent>
|
||||||
{!isLoading && <DialogContentText>{contentText}</DialogContentText>}
|
{!isLoading && <div>{contentText}</div>}
|
||||||
{isLoading && <Spinner />}
|
{isLoading && <Loading size='xl' />}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onCancel} variant="outlined" startIcon={<Icon iconName={IconName.XMARK} />}>
|
<Button onClick={onCancel} startContent={<Icon iconName={IconName.XMARK} />}>
|
||||||
No
|
No
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
variant="contained"
|
startContent={<Icon iconName={IconName.CIRCLE_CHECK} />}
|
||||||
startIcon={<Icon iconName={IconName.CIRCLE_CHECK} />}
|
|
||||||
>
|
>
|
||||||
Yes
|
Yes
|
||||||
</Button>
|
</Button>
|
||||||
61
client/src/components/flights/Flights.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Card, Skeleton } from "@noahspan/noahspan-components";
|
||||||
|
import LogbookCard from "../logbookCard/LogbookCard";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useLogs } from "../../hooks/logs/UseLogs";
|
||||||
|
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
||||||
|
|
||||||
|
const Flights = () => {
|
||||||
|
const [flights, setFlights] = useState<ILogbookEntry[]>([]);
|
||||||
|
const { logs, isLoading } = useLogs();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(logs)
|
||||||
|
const flights: ILogbookEntry[] | undefined = logs?.filter((log: ILogbookEntry) => {
|
||||||
|
if (log.tracks && log.tracks.length > 0) {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (flights && flights.length > 0) {
|
||||||
|
setFlights(flights)
|
||||||
|
}
|
||||||
|
}, [logs])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='max-w-screen-lg mx-auto'>
|
||||||
|
<div className='prose mt-5 mb-5'>
|
||||||
|
<h1>Flights</h1>
|
||||||
|
</div>
|
||||||
|
{!isLoading &&
|
||||||
|
<div>
|
||||||
|
<LogbookCard logs={flights} mode='flights' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!isLoading && [...Array(6)].map((_element, index) => {
|
||||||
|
return (
|
||||||
|
<div className='mb-5'>
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<div className='p-5'>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[60px]' width='w-[200px]' /></div>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[30px]' width='w-[200px]' /></div>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[300px]' width='w-[300px]' /></div>
|
||||||
|
{[...Array(4)].map((_element, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Flights;
|
||||||
@@ -2,8 +2,11 @@ import { Alert } from "../../interfaces/Alert.interface";
|
|||||||
|
|
||||||
export interface ILogFormState {
|
export interface ILogFormState {
|
||||||
alert: Alert | undefined;
|
alert: Alert | undefined;
|
||||||
|
experienceCollapseOpen: boolean;
|
||||||
|
instrumentCollapseOpen: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
landingsCollapseOpen: boolean;
|
||||||
pilotOptions: { label: string; value: string }[];
|
pilotOptions: { label: string; value: string }[];
|
||||||
selectedPilotName: string;
|
selectedPilotName: string;
|
||||||
}
|
}
|
||||||
@@ -3,15 +3,21 @@ import { ILogFormState } from './ILogFormState';
|
|||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
|
| { type: 'SET_EXPERIENCE_COLLAPSE_OPEN'; payload: boolean }
|
||||||
|
| { type: 'SET_INSTRUMENT_COLLAPSE_OPEN'; payload: boolean }
|
||||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_LANDINGS_COLLAPSE_OPEN'; payload: boolean }
|
||||||
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
||||||
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
||||||
|
|
||||||
export const initialState: ILogFormState = {
|
export const initialState: ILogFormState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
|
experienceCollapseOpen: true,
|
||||||
|
instrumentCollapseOpen: false,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
landingsCollapseOpen: true,
|
||||||
pilotOptions: [],
|
pilotOptions: [],
|
||||||
selectedPilotName: ''
|
selectedPilotName: ''
|
||||||
};
|
};
|
||||||
@@ -27,18 +33,36 @@ export const reducer = (
|
|||||||
alert: action.payload
|
alert: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_EXPERIENCE_COLLAPSE_OPEN': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
experienceCollapseOpen: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'SET_IS_DISABLED': {
|
case 'SET_IS_DISABLED': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isDisabled: action.payload
|
isDisabled: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_INSTRUMENT_COLLAPSE_OPEN': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
instrumentCollapseOpen: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'SET_IS_LOADING': {
|
case 'SET_IS_LOADING': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isLoading: action.payload
|
isLoading: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_LANDINGS_COLLAPSE_OPEN': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
landingsCollapseOpen: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'SET_PILOT_OPTIONS': {
|
case 'SET_PILOT_OPTIONS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -2,8 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
|
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
import { useAuth } from 'react-oidc-context'
|
||||||
import { useIsAuthenticated } from '@azure/msal-react';
|
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||||
import ReactLeafletKml from 'react-leaflet-kml';
|
import ReactLeafletKml from 'react-leaflet-kml';
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
@@ -15,8 +14,7 @@ import 'leaflet/dist/leaflet.css';
|
|||||||
const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
||||||
const [kmls, setKmls] = useState<any[]>([])
|
const [kmls, setKmls] = useState<any[]>([])
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const { getAccessToken } = useAccessToken();
|
const auth = useAuth()
|
||||||
const isAuthenticated = useIsAuthenticated();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(tracks)
|
console.log(tracks)
|
||||||
@@ -26,12 +24,13 @@ const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
|||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
const trackUrlSplit = track.url.split('/')
|
const trackUrlSplit = track.url.split('/')
|
||||||
const filename = trackUrlSplit[trackUrlSplit.length - 1];
|
const filename = trackUrlSplit[trackUrlSplit.length - 1];
|
||||||
const config = isAuthenticated
|
|
||||||
? { headers: { Authorization: await getAccessToken() } }
|
|
||||||
: {};
|
|
||||||
const response: AxiosResponse = await httpClient.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`api/tracks/${logId}/${filename}`,
|
`api/tracks/${logId}/${filename}`,
|
||||||
config
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: auth.user?.access_token
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const kml = new DOMParser().parseFromString(response.data, 'text/xml')
|
const kml = new DOMParser().parseFromString(response.data, 'text/xml')
|
||||||
|
|
||||||
@@ -1,27 +1,23 @@
|
|||||||
import { useEffect, useReducer } from "react";
|
import { useEffect, useReducer } from "react";
|
||||||
import { Button, Drawer, Grid, Icon, IconButton, IconName, Spinner, TextField, theme, Typography, useMediaQuery } from "@noahspan/noahspan-components";
|
import { Button, Drawer, Icon, IconButton, IconName, Input, Loading } from "@noahspan/noahspan-components";
|
||||||
import { useHttpClient } from "../../hooks/httpClient/UseHttpClient";
|
import { useHttpClient } from "../../hooks/httpClient/UseHttpClient";
|
||||||
import { AxiosInstance, AxiosResponse } from "axios";
|
import { AxiosInstance, AxiosResponse } from "axios";
|
||||||
import { useAccessToken } from "../../hooks/accessToken/UseAcessToken";
|
|
||||||
import { LogTracksProps } from "./LogTracksProps.interface";
|
import { LogTracksProps } from "./LogTracksProps.interface";
|
||||||
import { FormMode } from "../../enums/formMode";
|
import { FormMode } from "../../enums/formMode";
|
||||||
import { useIsAuthenticated } from "@azure/msal-react";
|
|
||||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
||||||
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
||||||
import { initialState, reducer } from "./reducer";
|
import { initialState, reducer } from "./reducer";
|
||||||
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
|
|
||||||
const LogTracks = ({ isDrawerOpen, mode, onOpenClose, selectedLogId }: LogTracksProps) => {
|
const LogTracks = ({ isDrawerOpen, mode, onOpenClose, selectedLogId }: LogTracksProps) => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState)
|
const [state, dispatch] = useReducer(reducer, initialState)
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const { getAccessToken } = useAccessToken();
|
const auth = useAuth();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
|
||||||
|
|
||||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
|
||||||
|
|
||||||
const getConfig = async () => {
|
const getConfig = async () => {
|
||||||
const config = isAuthenticated
|
const config = auth.isAuthenticated
|
||||||
? { headers: { Authorization: await getAccessToken() } }
|
? { headers: { Authorization: auth.user?.access_token } }
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -131,33 +127,28 @@ const LogTracks = ({ isDrawerOpen, mode, onOpenClose, selectedLogId }: LogTracks
|
|||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={isDrawerOpen}
|
open={isDrawerOpen}
|
||||||
anchor='right'
|
position='right'
|
||||||
PaperProps={{
|
width='25%'
|
||||||
sx: {
|
|
||||||
padding: '30px',
|
|
||||||
width: isMedium ? '33%' : '75%'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Grid container spacing={2}>
|
<div>
|
||||||
<Grid size={11}>
|
<div>
|
||||||
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Tracks`}</Typography>
|
<h4>{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Tracks`}</h4>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid display="flex" justifyContent="right" size={1}>
|
<div>
|
||||||
<IconButton disabled={state.isLoading ? true : false} onClick={onCancel}>
|
<IconButton disabled={state.isLoading ? true : false} onClick={onCancel}>
|
||||||
<Icon iconName={IconName.XMARK} />
|
<Icon iconName={IconName.XMARK} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</div>
|
||||||
{mode === FormMode.EDIT &&
|
{mode === FormMode.EDIT &&
|
||||||
<>
|
<>
|
||||||
{state.isLoading &&
|
{state.isLoading &&
|
||||||
<>
|
<>
|
||||||
<Grid display="flex" justifyContent="center" size={12}>
|
<div>
|
||||||
<Spinner />
|
<Loading size='xl' />
|
||||||
</Grid>
|
</div>
|
||||||
<Grid display="flex" justifyContent="center" size={12}>
|
<div>
|
||||||
Loading...
|
Loading...
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{!state.isLoading && state.tracks.length > 0 && state.tracks.map((track, index) => {
|
{!state.isLoading && state.tracks.length > 0 && state.tracks.map((track, index) => {
|
||||||
@@ -166,43 +157,40 @@ const LogTracks = ({ isDrawerOpen, mode, onOpenClose, selectedLogId }: LogTracks
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid size={11}>
|
<div>
|
||||||
<TextField disabled={true} fullWidth value={filename} />
|
<Input disabled={true} value={filename} />
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={1}>
|
<div>
|
||||||
<IconButton onClick={() => onDeleteTrack(filename, index)}><Icon iconName={IconName.TRASH} /></IconButton>
|
<IconButton onClick={() => onDeleteTrack(filename, index)}><Icon iconName={IconName.TRASH} /></IconButton>
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Grid display='flex' gap={2} justifyContent='right' size={12}>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
disabled={state.isLoading ? true : false}
|
disabled={state.isLoading ? true : false}
|
||||||
startIcon={<Icon iconName={IconName.XMARK} />}
|
startContent={<Icon iconName={IconName.XMARK} />}
|
||||||
variant="outlined"
|
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
size="small"
|
size='sm'
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
{mode.toString() !== FormMode.VIEW && (
|
{mode.toString() !== FormMode.VIEW && (
|
||||||
<Button
|
<Button
|
||||||
component='label'
|
|
||||||
disabled={state.isLoading ? true : false}
|
disabled={state.isLoading ? true : false}
|
||||||
startIcon={<Icon iconName={IconName.UPLOAD} />}
|
startContent={<Icon iconName={IconName.UPLOAD} />}
|
||||||
variant='contained'
|
|
||||||
>
|
>
|
||||||
Upload Track
|
Upload Track
|
||||||
{/* <input hidden onChange={handleFileUpload} type='file' /> */}
|
{/* <input hidden onChange={handleFileUpload} type='file' /> */}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{mode === FormMode.VIEW &&
|
{mode === FormMode.VIEW &&
|
||||||
<LogTrackMaps logId={selectedLogId!} tracks={state.tracks} />
|
<LogTrackMaps logId={selectedLogId!} tracks={state.tracks} />
|
||||||
}
|
}
|
||||||
</Grid>
|
</div>
|
||||||
{/* {state.isConfirmDialogOpen && (
|
{/* {state.isConfirmDialogOpen && (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
contentText="Are you sure you want to delete this track?"
|
contentText="Are you sure you want to delete this track?"
|
||||||
@@ -2,24 +2,17 @@ import { useEffect, useReducer } from 'react';
|
|||||||
import LogForm from '../logForm/LogForm';
|
import LogForm from '../logForm/LogForm';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
Grid,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconName,
|
IconName,
|
||||||
Spinner,
|
Loading,
|
||||||
Table,
|
Table
|
||||||
theme,
|
|
||||||
Typography,
|
|
||||||
useMediaQuery
|
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
|
||||||
import { useIsAuthenticated } from '@azure/msal-react';
|
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { authColumns, unauthColumns } from './columns';
|
import { authColumns, unauthColumns } from './columns';
|
||||||
import ActionMenu from '../actionMenu/ActionMenu';
|
import ActionMenu from '../actionMenu/ActionMenu';
|
||||||
@@ -27,13 +20,12 @@ import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
|||||||
import { ILogbookEntry } from './ILogbookEntry';
|
import { ILogbookEntry } from './ILogbookEntry';
|
||||||
import LogbookCard from '../logbookCard/LogbookCard';
|
import LogbookCard from '../logbookCard/LogbookCard';
|
||||||
import LogTracks from '../logTracks/LogTracks';
|
import LogTracks from '../logTracks/LogTracks';
|
||||||
|
import { useOidc } from '../../auth/oidcConfig';
|
||||||
|
|
||||||
const Logbook: React.FC<unknown> = () => {
|
const Logbook: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const { isUserLoggedIn } = useOidc()
|
||||||
const { getAccessToken } = useAccessToken();
|
|
||||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
|
||||||
const actionsColumn: ColumnDef<ILogbookEntry> = {
|
const actionsColumn: ColumnDef<ILogbookEntry> = {
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
meta: {
|
meta: {
|
||||||
@@ -65,10 +57,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||||
|
|
||||||
const config = isAuthenticated
|
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
||||||
? { headers: { Authorization: await getAccessToken() } }
|
|
||||||
: {};
|
|
||||||
const response: AxiosResponse = await httpClient.get(`api/logs`, config);
|
|
||||||
const entries: ILogbookEntry[] = response.data;
|
const entries: ILogbookEntry[] = response.data;
|
||||||
console.log(entries)
|
console.log(entries)
|
||||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
@@ -162,12 +151,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
||||||
|
|
||||||
const token = await getAccessToken();
|
await httpClient.delete(`api/logs/${state.selectedLogId}`);
|
||||||
const config = isAuthenticated
|
|
||||||
? { headers: { Authorization: `${token}` } }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
await httpClient.delete(`api/logs/${state.selectedLogId}`, config);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_DELETE',
|
type: 'SET_DELETE',
|
||||||
@@ -196,7 +180,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let newColumns: ColumnDef<ILogbookEntry>[];
|
let newColumns: ColumnDef<ILogbookEntry>[];
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isUserLoggedIn) {
|
||||||
newColumns = [...authColumns];
|
newColumns = [...authColumns];
|
||||||
} else {
|
} else {
|
||||||
newColumns = [...unauthColumns];
|
newColumns = [...unauthColumns];
|
||||||
@@ -216,7 +200,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SET_COLUMNS', payload: newColumns })
|
dispatch({ type: 'SET_COLUMNS', payload: newColumns })
|
||||||
}, [isAuthenticated])
|
}, [isUserLoggedIn])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isFormOpen) {
|
if (!state.isFormOpen) {
|
||||||
@@ -225,57 +209,56 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
}, [state.isFormOpen, state.isTracksOpen]);
|
}, [state.isFormOpen, state.isTracksOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ margin: '20px' }}>
|
<>
|
||||||
<Grid container spacing={2}>
|
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||||
<Grid size={isMedium ? 11 : 6}>
|
<div className='prose max-w-none col-span-10 mt-5 mb-5'>
|
||||||
<Typography variant="h4">Logbook</Typography>
|
<h1>Logbook</h1>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid display="flex" justifyContent="right" size={isMedium ? 1 : 6}>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
{isAuthenticated &&
|
{isUserLoggedIn &&
|
||||||
<Button
|
<Button
|
||||||
|
color='primary'
|
||||||
onClick={() => onOpenCloseLogForm(FormMode.ADD)}
|
onClick={() => onOpenCloseLogForm(FormMode.ADD)}
|
||||||
startIcon={<Icon iconName={IconName.PLUS} />}
|
startContent={<Icon iconName={IconName.PLUS} />}
|
||||||
variant="contained"
|
|
||||||
data-testid="pilot-add-button"
|
data-testid="pilot-add-button"
|
||||||
>
|
>
|
||||||
Add Entry
|
Add Entry
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
</Grid>
|
</div>
|
||||||
{!state.isLoading && state.alert && (
|
{!state.isLoading && state.alert && (
|
||||||
<Grid display="flex" justifyContent="center" size={12}>
|
<div>
|
||||||
<Alert
|
<Alert
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
severity={state.alert.severity}
|
||||||
sx={{ width: '100%' }}
|
|
||||||
>
|
>
|
||||||
{state.alert.message}
|
{state.alert.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Grid>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!state.isLoading && (
|
{!state.isLoading && (
|
||||||
<Grid size={12}>
|
<div>
|
||||||
{isMedium && state.columns && state.columns.length > 0 && state.entries.length > 0 && (
|
{state.columns && state.columns.length > 0 && state.entries.length > 0 && (
|
||||||
<Table columns={state.columns} data={state.entries} />
|
<Table columns={state.columns} data={state.entries} />
|
||||||
)}
|
)}
|
||||||
{!isMedium && state.entries.length > 0 &&
|
{state.entries.length > 0 &&
|
||||||
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseLogForm} />
|
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseLogForm} />
|
||||||
}
|
}
|
||||||
</Grid>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.isLoading && !state.alert && (
|
{state.isLoading && !state.alert && (
|
||||||
<>
|
<>
|
||||||
<Grid display="flex" justifyContent="center" size={12}>
|
<div>
|
||||||
<Spinner />
|
<Loading size='xl' />
|
||||||
</Grid>
|
</div>
|
||||||
<Grid display="flex" justifyContent="center" size={12}>
|
<div>
|
||||||
Loading...
|
Loading...
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</div>
|
||||||
{state.isFormOpen && (
|
{state.isFormOpen && (
|
||||||
<LogForm
|
<LogForm
|
||||||
logId={state.selectedLogId}
|
logId={state.selectedLogId}
|
||||||
@@ -302,7 +285,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
selectedLogId={state.selectedLogId}
|
selectedLogId={state.selectedLogId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
93
client/src/components/logbookCard/LogbookCard.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Card, CardBody, CardContent, CardHeader } from "@noahspan/noahspan-components";
|
||||||
|
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
||||||
|
import ActionMenu from "../actionMenu/ActionMenu";
|
||||||
|
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
||||||
|
|
||||||
|
|
||||||
|
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{logs.map((log) => {
|
||||||
|
const date = new Date(log.date);
|
||||||
|
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card key={log.id}>
|
||||||
|
<CardBody>
|
||||||
|
<CardHeader><ActionMenu id={log.id} onDelete={onDelete!} onOpenCloseForm={onOpenCloseForm!} /></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div>
|
||||||
|
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
||||||
|
<div>
|
||||||
|
<LogTrackMaps
|
||||||
|
logId={log.id}
|
||||||
|
tracks={log.tracks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<span>Aircraft Make and Model</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{log.aircraftMakeModel}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Route From</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{log.routeFrom}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Route To</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{log.routeTo}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Duration Of Flight</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{log.durationOfFlight}</span>
|
||||||
|
</div>
|
||||||
|
{mode === 'logbook' && log.tracks && log.tracks.length > 0 &&
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span>Tracks</span>
|
||||||
|
</div>
|
||||||
|
{log.tracks.map((track: { id: string; order: number; url: string}) => {
|
||||||
|
const trackSplit = track.url.split('/')
|
||||||
|
const filename = trackSplit[trackSplit.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>{filename}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{log.notes &&
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span>Notes</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{log.notes}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogbookCard;
|
||||||
26
client/src/components/pilotCard/PilotCard.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Card, CardActions, CardBody, CardHeader} from "@noahspan/noahspan-components"
|
||||||
|
import { PilotCardProps } from "./PilotCardProps.interface"
|
||||||
|
import ActionMenu from "../actionMenu/ActionMenu"
|
||||||
|
|
||||||
|
const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{pilots.map((pilot) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card key={pilot.id}>
|
||||||
|
<CardBody>
|
||||||
|
<CardHeader>{pilot.name}</CardHeader>
|
||||||
|
<CardActions>
|
||||||
|
<ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
||||||
|
</CardActions>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PilotCard;
|
||||||
@@ -3,27 +3,24 @@ import { useForm, Controller, FormProvider } from 'react-hook-form';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Drawer,
|
Drawer,
|
||||||
Grid,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconName,
|
IconName,
|
||||||
|
Input,
|
||||||
PeoplePicker,
|
PeoplePicker,
|
||||||
StateSelect,
|
StateSelect
|
||||||
TextField,
|
|
||||||
theme,
|
|
||||||
Typography,
|
|
||||||
useMediaQuery
|
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { IPilotFormProps } from './IPilotFormProps';
|
import { IPilotFormProps } from './IPilotFormProps';
|
||||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
|
||||||
import { useIsAuthenticated } from '@azure/msal-react';
|
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Person } from '@microsoft/microsoft-graph-types';
|
import { Person } from '@microsoft/microsoft-graph-types';
|
||||||
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||||
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||||
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||||
|
import { useOidc } from '../../auth/oidcConfig';
|
||||||
|
import { getOidc } from '../../auth/oidcConfig';
|
||||||
|
// import httpClient from '../../httpClient/httpClient';
|
||||||
|
|
||||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||||
pilotId,
|
pilotId,
|
||||||
@@ -31,7 +28,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
mode,
|
mode,
|
||||||
onOpenClose
|
onOpenClose
|
||||||
}: IPilotFormProps) => {
|
}: IPilotFormProps) => {
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const [peoplePickerValue, setPeoplePickerValue] = useState<string>('');
|
||||||
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
|
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
|
||||||
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
|
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
@@ -39,8 +36,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
displayName: ''
|
displayName: ''
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const { getAccessToken } = useAccessToken();
|
const { isUserLoggedIn } = useOidc();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
name: '',
|
name: '',
|
||||||
address: '',
|
address: '',
|
||||||
@@ -55,27 +51,26 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
});
|
});
|
||||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||||
const [isError, setIsError] = useState<boolean>(false);
|
const [isError, setIsError] = useState<boolean>(false);
|
||||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
const httpClient = useHttpClient();
|
||||||
|
|
||||||
const onPeoplePickerSearch = async (
|
const onPeoplePickerSearch = async (
|
||||||
_event: React.SyntheticEvent,
|
|
||||||
value: string
|
value: string
|
||||||
) => {
|
) => {
|
||||||
setIsPeoplePickerLoading(true);
|
setIsPeoplePickerLoading(true);
|
||||||
|
setPeoplePickerValue(value)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (value !== '') {
|
if (value !== '') {
|
||||||
const searchString: string = value;
|
const searchString: string = value;
|
||||||
const accessToken: string = await getAccessToken();
|
const oidc = await getOidc();
|
||||||
const response: AxiosResponse = await httpClient.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`api/user/search?search=${searchString}`,
|
`api/msgraph/search?search=${searchString}`, {
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: accessToken
|
Authorization: oidc.isUserLoggedIn ? `Bearer ${(await oidc.getTokens()).accessToken}` : ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
console.log(response)
|
||||||
setPeoplePickerResults(response.data);
|
setPeoplePickerResults(response.data);
|
||||||
} else {
|
} else {
|
||||||
setPeoplePickerResults([]);
|
setPeoplePickerResults([]);
|
||||||
@@ -87,13 +82,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPeoplePickerSelectionChange = (
|
const onPersonSelected = (person: Person) => {
|
||||||
_event: React.SyntheticEvent,
|
methods.setValue('name', person.displayName!.toString());
|
||||||
value: Person,
|
setPeoplePickerValue(person.displayName!);
|
||||||
_reason: string
|
setPeoplePickerResults([])
|
||||||
) => {
|
|
||||||
methods.setValue('name', value.displayName!.toString());
|
|
||||||
setSelectedPerson(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
@@ -107,20 +99,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const accessToken: string = await getAccessToken();
|
|
||||||
|
|
||||||
if (!pilotId) {
|
if (!pilotId) {
|
||||||
await httpClient.post(`api/pilots`, data, {
|
await httpClient.post(`api/pilots`, data);
|
||||||
headers: {
|
|
||||||
Authorization: accessToken
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await httpClient.put(`api/pilots/pilot/${pilotId}`, data, {
|
await httpClient.put(`api/pilots/pilot/${pilotId}`, data)
|
||||||
headers: {
|
|
||||||
Authorization: accessToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
methods.reset(defaultValues)
|
methods.reset(defaultValues)
|
||||||
@@ -146,12 +128,8 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const config = isAuthenticated
|
|
||||||
? { headers: { Authorization: await getAccessToken() } }
|
|
||||||
: {};
|
|
||||||
const response: AxiosResponse = await httpClient.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`api/pilots/${pilotId}`,
|
`api/pilots/${pilotId}`
|
||||||
config
|
|
||||||
);
|
);
|
||||||
const pilot = response.data;
|
const pilot = response.data;
|
||||||
|
|
||||||
@@ -174,54 +152,49 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={isDrawerOpen}
|
open={isDrawerOpen}
|
||||||
anchor="right"
|
position='right'
|
||||||
data-testid="pilot-drawer"
|
data-testid="pilot-drawer"
|
||||||
PaperProps={{
|
width='50%'
|
||||||
sx: {
|
|
||||||
padding: '30px',
|
|
||||||
width: isMedium ? '33%' : '75%'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||||
<Grid container spacing={2}>
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
<Grid size={11}>
|
<div className='col-span-10 self-center'>
|
||||||
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</Typography>
|
<h2 style={{ margin: 0 }}>{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</h2>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid display="flex" justifyContent="right" size={1}>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
<IconButton onClick={onCancel}>
|
<IconButton onClick={onCancel}>
|
||||||
<Icon iconName={IconName.XMARK} />
|
<Icon iconName={IconName.XMARK} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">Name *</Typography>
|
<h6>Name *</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<PeoplePicker
|
<PeoplePicker
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
loading={isPeoplePickerLoading}
|
// loading={isPeoplePickerLoading}
|
||||||
onInputChanged={onPeoplePickerSearch}
|
onInputChanged={onPeoplePickerSearch}
|
||||||
onSelectionChanged={onPeoplePickerSelectionChange}
|
onPersonSelected={onPersonSelected}
|
||||||
options={peoplePickerResults}
|
people={peoplePickerResults}
|
||||||
value={selectedPerson}
|
value={peoplePickerValue}
|
||||||
|
width='w-full'
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
{isAuthenticated &&
|
{isUserLoggedIn &&
|
||||||
<>
|
<>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">Address *</Typography>
|
<h6>Address *</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Controller
|
||||||
name="address"
|
name="address"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'An address is required' }}
|
rules={{ required: 'An address is required' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextField
|
<Input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
error={methods.formState.errors.address ? true : false}
|
color={methods.formState.errors.address ? 'error' : undefined}
|
||||||
fullWidth
|
|
||||||
helperText={
|
helperText={
|
||||||
methods.formState.errors.address
|
methods.formState.errors.address
|
||||||
? methods.formState.errors.address.message
|
? methods.formState.errors.address.message
|
||||||
@@ -229,23 +202,23 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
width='w-full'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">City *</Typography>
|
<h6>City *</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Controller
|
||||||
name="city"
|
name="city"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'A city is required' }}
|
rules={{ required: 'A city is required' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextField
|
<Input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
error={methods.formState.errors.city ? true : false}
|
color={methods.formState.errors.city ? 'error' : undefined}
|
||||||
fullWidth
|
|
||||||
helperText={
|
helperText={
|
||||||
methods.formState.errors.city
|
methods.formState.errors.city
|
||||||
? methods.formState.errors.city.message
|
? methods.formState.errors.city.message
|
||||||
@@ -253,14 +226,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
width='w-full'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">State *</Typography>
|
<h6>State *</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Controller
|
||||||
name="state"
|
name="state"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
@@ -269,7 +243,6 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
<StateSelect
|
<StateSelect
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
// error={methods.formState.errors.state ? true : false}
|
// error={methods.formState.errors.state ? true : false}
|
||||||
fullWidth
|
|
||||||
// helperText={
|
// helperText={
|
||||||
// methods.formState.errors.state
|
// methods.formState.errors.state
|
||||||
// ? methods.formState.errors.state.message?.toString()
|
// ? methods.formState.errors.state.message?.toString()
|
||||||
@@ -277,25 +250,24 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
// }
|
// }
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
variant="outlined"
|
width='w-full'
|
||||||
data-testid="pilot-form-state-dropdown"
|
data-testid="pilot-form-state-dropdown"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">Postal Code *</Typography>
|
<h6>Postal Code *</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Controller
|
||||||
name="postalCode"
|
name="postalCode"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'A postal code is required' }}
|
rules={{ required: 'A postal code is required' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextField
|
<Input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
error={methods.formState.errors.postalCode ? true : false}
|
color={methods.formState.errors.postalCode ? 'error' : undefined}
|
||||||
fullWidth
|
|
||||||
helperText={
|
helperText={
|
||||||
methods.formState.errors.postalCode
|
methods.formState.errors.postalCode
|
||||||
? methods.formState.errors.postalCode.message
|
? methods.formState.errors.postalCode.message
|
||||||
@@ -303,14 +275,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
width='w-full'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">Email</Typography>
|
<h6>Email</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Controller
|
||||||
name="email"
|
name="email"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
@@ -321,10 +294,9 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextField
|
<Input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
fullWidth
|
color={methods.formState.errors.email ? 'error' : undefined}
|
||||||
error={methods.formState.errors.email ? true : false}
|
|
||||||
helperText={
|
helperText={
|
||||||
methods.formState.errors.email
|
methods.formState.errors.email
|
||||||
? methods.formState.errors.email.message
|
? methods.formState.errors.email.message
|
||||||
@@ -332,14 +304,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
width='w-full'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div className='col-span-3 self-center'>
|
||||||
<Typography variant="h6">Phone Number</Typography>
|
<h6>Phone Number</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Controller
|
||||||
name="phone"
|
name="phone"
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
@@ -350,10 +323,9 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextField
|
<Input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
fullWidth
|
color={methods.formState.errors.phone ? 'error' : undefined}
|
||||||
error={methods.formState.errors.phone ? true : false}
|
|
||||||
helperText={
|
helperText={
|
||||||
methods.formState.errors.phone
|
methods.formState.errors.phone
|
||||||
? methods.formState.errors.phone.message
|
? methods.formState.errors.phone.message
|
||||||
@@ -361,10 +333,11 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
width='w-full'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{/* {isAuthenticated &&
|
{/* {isAuthenticated &&
|
||||||
@@ -380,35 +353,33 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
||||||
</Grid> */}
|
</Grid> */}
|
||||||
<Grid display="flex" gap={2} justifyContent="right" size={12}>
|
<div className='col-span-12 justify-self-end self-center'>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
isDisabled && mode.toString() !== FormMode.VIEW
|
isDisabled && mode.toString() !== FormMode.VIEW
|
||||||
? isDisabled
|
? isDisabled
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
startIcon={<Icon iconName={IconName.XMARK} />}
|
startContent={<Icon iconName={IconName.XMARK} />}
|
||||||
variant="outlined"
|
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
outline={true}
|
||||||
data-testid="pilot-cancel-button"
|
data-testid="pilot-cancel-button"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||||
</Button>
|
</Button>
|
||||||
{mode.toString() !== FormMode.VIEW && (
|
{mode.toString() !== FormMode.VIEW && (
|
||||||
<Button
|
<Button
|
||||||
|
color='primary'
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
startIcon={<Icon iconName={IconName.SAVE} />}
|
startContent={<Icon iconName={IconName.SAVE} />}
|
||||||
size="small"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
|
||||||
data-testid="pilot-save-button"
|
data-testid="pilot-save-button"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</div>
|
||||||
</Grid>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@@ -2,13 +2,11 @@ import { PilotFormCertificatesProps } from './PilotFormCertificatesProps.interfa
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Grid,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconName,
|
IconName,
|
||||||
Select,
|
Input,
|
||||||
TextField,
|
Select
|
||||||
Typography,
|
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
@@ -28,32 +26,29 @@ const PilotFormCertificates = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<div>
|
||||||
container
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
{fields.length > 0 || mode !== FormMode.VIEW &&
|
||||||
<Grid size={12}>
|
<div>
|
||||||
<Typography variant="h5">Certificates</Typography>
|
<h5>Certificates</h5>
|
||||||
</Grid>
|
</div>
|
||||||
}
|
}
|
||||||
{fields.length > 0 && (
|
{fields.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Grid size={4}>
|
<div>
|
||||||
<Typography variant="h6">Type</Typography>
|
<h6>Type</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={4}>
|
<div>
|
||||||
<Typography variant="h6">Number</Typography>
|
<h6>Number</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={3}>
|
<div>
|
||||||
<Typography variant="h6">Date of Issue</Typography>
|
<h6>Date of Issue</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={1}>
|
<div>
|
||||||
</Grid>
|
</div>
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid size={4}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name={`certificates.${index}.type`}
|
name={`certificates.${index}.type`}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -61,7 +56,6 @@ const PilotFormCertificates = ({
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
fullWidth
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
@@ -92,24 +86,23 @@ const PilotFormCertificates = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={4}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name={`certificates.${index}.number`}
|
name={`certificates.${index}.number`}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<Input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
fullWidth
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={3}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name={`certificates.${index}.dateOfIssue`}
|
name={`certificates.${index}.dateOfIssue`}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -123,25 +116,22 @@ const PilotFormCertificates = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={1}>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
sx={{
|
|
||||||
marginTop: '-5px'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon iconName={IconName.TRASH} size='sm' />
|
<Icon iconName={IconName.TRASH} size='sm' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isDisabled &&
|
{!isDisabled &&
|
||||||
<Grid display="flex" justifyContent="right" size={12}>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
append({
|
append({
|
||||||
@@ -150,14 +140,13 @@ const PilotFormCertificates = ({
|
|||||||
dateOfIssue: null
|
dateOfIssue: null
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
startIcon={<Icon iconName={IconName.PLUS} />}
|
startContent={<Icon iconName={IconName.PLUS} />}
|
||||||
variant="contained"
|
|
||||||
>
|
>
|
||||||
Add Certificate
|
Add Certificate
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</div>
|
||||||
}
|
}
|
||||||
</Grid>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2,12 +2,10 @@ import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interfa
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Grid,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconName,
|
IconName,
|
||||||
Select,
|
Select
|
||||||
Typography
|
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
@@ -28,28 +26,25 @@ const PilotFormEndorsements = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<div>
|
||||||
container
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
{fields.length > 0 || mode !== FormMode.VIEW &&
|
||||||
<Grid size={12}>
|
<div>
|
||||||
<Typography variant="h5">Endorsements</Typography>
|
<h5>Endorsements</h5>
|
||||||
</Grid>
|
</div>
|
||||||
}
|
}
|
||||||
{fields.length > 0 && (
|
{fields.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Grid size={8}>
|
<div>
|
||||||
<Typography variant="h6">Type</Typography>
|
<h6>Type</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={3}>
|
<div>
|
||||||
<Typography variant="h6">Date of Issue</Typography>
|
<h6>Date of Issue</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={1}></Grid>
|
<div></div>
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid size={8}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name={`endorsements.${index}.type`}
|
name={`endorsements.${index}.type`}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -57,7 +52,6 @@ const PilotFormEndorsements = ({
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
fullWidth
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
@@ -84,8 +78,8 @@ const PilotFormEndorsements = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={3}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name={`endorsements.${index}.dateOfIssue`}
|
name={`endorsements.${index}.dateOfIssue`}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -99,25 +93,22 @@ const PilotFormEndorsements = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={1}>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
sx={{
|
|
||||||
marginTop: '-5px'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon iconName={IconName.TRASH} size="sm" />
|
<Icon iconName={IconName.TRASH} size="sm" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isDisabled &&
|
{!isDisabled &&
|
||||||
<Grid display="flex" justifyContent="right" size={12}>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
append({
|
append({
|
||||||
@@ -125,14 +116,13 @@ const PilotFormEndorsements = ({
|
|||||||
dateOfIssue: null
|
dateOfIssue: null
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
startIcon={<Icon iconName={IconName.PLUS} />}
|
startContent={<Icon iconName={IconName.PLUS} />}
|
||||||
variant="contained"
|
|
||||||
>
|
>
|
||||||
Add Endorsement
|
Add Endorsement
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</div>
|
||||||
}
|
}
|
||||||
</Grid>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatePicker, Grid, Select, theme, Typography, useMediaQuery } from '@noahspan/noahspan-components';
|
import { DatePicker, Select } from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFormContext } from "react-hook-form"
|
import { Controller, useFormContext } from "react-hook-form"
|
||||||
import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
||||||
|
|
||||||
@@ -8,17 +8,16 @@ const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
setValue
|
setValue
|
||||||
} = useFormContext();
|
} = useFormContext();
|
||||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<div>
|
||||||
<Grid size={12}>
|
<div>
|
||||||
<Typography variant="h5">Medical</Typography>
|
<h5>Medical</h5>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div>
|
||||||
<Typography variant="h6">Class</Typography>
|
<h6>Class</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name="medicalClass"
|
name="medicalClass"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -26,7 +25,6 @@ const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
fullWidth
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
@@ -53,11 +51,11 @@ const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 3 : 12}>
|
<div>
|
||||||
<Typography variant="h6">Expiration</Typography>
|
<h6>Expiration</h6>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={isMedium ? 9 : 12}>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
name="medicalExpiration"
|
name="medicalExpiration"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -71,8 +69,8 @@ const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
</Grid>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,34 +2,29 @@ import { useEffect, useReducer, useState } from 'react';
|
|||||||
import PilotForm from '../pilotForm/PilotForm';
|
import PilotForm from '../pilotForm/PilotForm';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
Grid,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconName,
|
IconName,
|
||||||
Table,
|
Table,
|
||||||
theme,
|
|
||||||
Typography,
|
|
||||||
useMediaQuery
|
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
|
||||||
import { useIsAuthenticated } from '@azure/msal-react';
|
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Pilot } from './Pilot.interface';
|
import { Pilot } from './Pilot.interface';
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import ActionMenu from '../actionMenu/ActionMenu';
|
import ActionMenu from '../actionMenu/ActionMenu';
|
||||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||||
import PilotCard from '../pilotCard/PilotCard';
|
import PilotCard from '../pilotCard/PilotCard';
|
||||||
|
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
||||||
|
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||||
|
import { UserRole } from '../../enums/userRole';
|
||||||
|
|
||||||
const Pilots: React.FC<unknown> = () => {
|
const Pilots: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const { isUserLoggedIn, decodedIdToken } = useOidc();
|
||||||
const { getAccessToken } = useAccessToken();
|
const userRole = useUserRole();
|
||||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
|
||||||
|
|
||||||
const getPilots = async () => {
|
const getPilots = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -98,12 +93,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
||||||
|
|
||||||
const token = await getAccessToken();
|
await httpClient.delete(`api/pilots/pilot/${state.selectedPilotId}`);
|
||||||
const config = isAuthenticated
|
|
||||||
? { headers: { Authorization: `${token}` } }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
await httpClient.delete(`api/pilots/pilot/${state.selectedPilotId}`, config);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_DELETE',
|
type: 'SET_DELETE',
|
||||||
@@ -154,45 +144,46 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
}, [state.isFormOpen]);
|
}, [state.isFormOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ margin: '20px' }}>
|
<>
|
||||||
<Grid container spacing={2}>
|
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||||
<Grid size={isMedium ? 11 : 6}>
|
<div className='prose max-w-none col-span-10 mt-5 mb-5' >
|
||||||
<Typography variant="h4">Pilots</Typography>
|
<h1>Pilots</h1>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid display="flex" justifyContent="right" size={isMedium ? 1 : 6}>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
{isAuthenticated &&
|
{userRole && userRole !== UserRole.READ &&
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
color='primary'
|
||||||
startIcon={<Icon iconName={IconName.PLUS} />}
|
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||||
variant="contained"
|
startContent={<Icon iconName={IconName.PLUS} />}
|
||||||
data-testid="pilot-add-button"
|
data-testid="pilot-add-button"
|
||||||
>
|
>
|
||||||
Add Pilot
|
Add Pilot
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
</Grid>
|
</div>
|
||||||
{!state.isLoading && state.alert && (
|
{!state.isLoading && state.alert && (
|
||||||
<Grid display="flex" justifyContent="center" size={12}>
|
<div>
|
||||||
<Alert
|
<Alert
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
severity={state.alert.severity}
|
||||||
sx={{ width: '100%' }}
|
>
|
||||||
>
|
{state.alert.message}
|
||||||
{state.alert.message}
|
</Alert>
|
||||||
</Alert>
|
</div>
|
||||||
</Grid>
|
)}
|
||||||
)}
|
<div className='col-span-12'>
|
||||||
<Grid size={12}>
|
{state.pilots.length > 0 &&
|
||||||
{isMedium && state.pilots.length > 0 &&
|
<Table columns={columns} data={state.pilots} />
|
||||||
<Table columns={columns} data={state.pilots} />
|
}
|
||||||
}
|
{state.pilots.length > 0 &&
|
||||||
{!isMedium && state.pilots.length > 0 &&
|
<div className='lg:hidden'>
|
||||||
<PilotCard pilots={state.pilots} onDelete={onDeleteEntry} onOpenCloseForm={onOpenClosePilotForm} />
|
<PilotCard pilots={state.pilots} onDelete={onDeleteEntry} onOpenCloseForm={onOpenClosePilotForm} />
|
||||||
}
|
</div>
|
||||||
</Grid>
|
}
|
||||||
</Grid>
|
</div>
|
||||||
|
</div>
|
||||||
{state.isFormOpen && (
|
{state.isFormOpen && (
|
||||||
<PilotForm
|
<PilotForm
|
||||||
isDrawerOpen={state.isFormOpen}
|
isDrawerOpen={state.isFormOpen}
|
||||||
@@ -211,8 +202,9 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
title="Confirm Delete"
|
title="Confirm Delete"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Pilots;
|
export default Pilots;
|
||||||
|
|
||||||
@@ -2,22 +2,15 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Dropdown,
|
||||||
Button,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
|
||||||
IconName,
|
IconName,
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Navbar,
|
Navbar,
|
||||||
Spinner,
|
|
||||||
Typography
|
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
|
||||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { User } from '@microsoft/microsoft-graph-types';
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
|
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
||||||
|
|
||||||
const SiteNav = () => {
|
const SiteNav = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
@@ -25,9 +18,7 @@ const SiteNav = () => {
|
|||||||
const [pages, setPages] = useState<{ name: string; url: string; }[]>([]);
|
const [pages, setPages] = useState<{ name: string; url: string; }[]>([]);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const { isUserLoggedIn, login, logout } = useOidc()
|
||||||
const { getAccessToken } = useAccessToken();
|
|
||||||
const { inProgress, instance } = useMsal();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const getPages = () => {
|
const getPages = () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
@@ -48,19 +39,23 @@ const SiteNav = () => {
|
|||||||
setPages(pages)
|
setPages(pages)
|
||||||
};
|
};
|
||||||
const handleSignIn = () => {
|
const handleSignIn = () => {
|
||||||
instance.loginRedirect({
|
// auth.signinRedirect();
|
||||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
// auth.signinRedirect({
|
||||||
});
|
// scope: `api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`
|
||||||
|
// })
|
||||||
|
login();
|
||||||
|
// console.log(auth.user?.access_token)
|
||||||
};
|
};
|
||||||
const handleSignOut = () => {
|
const handleSignOut = () => {
|
||||||
instance.logoutRedirect();
|
// auth.signoutRedirect();
|
||||||
|
logout()
|
||||||
};
|
};
|
||||||
const getUserProfile = async (accessToken: string): Promise<User> => {
|
const getUserProfile = async (): Promise<User> => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(`api/user/profile`, {
|
const response: AxiosResponse = await httpClient.get(`api/user/profile`, {
|
||||||
headers: {
|
// headers: {
|
||||||
Authorization: accessToken
|
// Authorization: `Bearer ${accessToken}`
|
||||||
}
|
// }
|
||||||
});
|
});
|
||||||
const userProfile: User = response.data;
|
const userProfile: User = response.data;
|
||||||
|
|
||||||
@@ -69,12 +64,12 @@ const SiteNav = () => {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
const getUserPhoto = async (): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(`api/user/photo`, {
|
const response: AxiosResponse = await httpClient.get(`api/user/photo`, {
|
||||||
headers: {
|
// headers: {
|
||||||
Authorization: accessToken
|
// Authorization: accessToken
|
||||||
},
|
// },
|
||||||
responseType: 'arraybuffer'
|
responseType: 'arraybuffer'
|
||||||
});
|
});
|
||||||
const arrayBufferView = new Uint8Array(response.data);
|
const arrayBufferView = new Uint8Array(response.data);
|
||||||
@@ -92,12 +87,12 @@ const SiteNav = () => {
|
|||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={handleSignOut}>
|
<div>
|
||||||
<Icon iconName={IconName.SIGN_OUT} />
|
<Icon iconName={IconName.SIGN_OUT} />
|
||||||
<Typography sx={{ marginLeft: '10px', textAlign: 'center' }}>
|
<span>
|
||||||
Sign Out
|
Sign Out
|
||||||
</Typography>
|
</span>
|
||||||
</MenuItem>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,16 +101,21 @@ const SiteNav = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const accessToken: string = await getAccessToken();
|
// const userProfile = await getUserProfile();
|
||||||
const userProfile = await getUserProfile(accessToken);
|
// const userPhoto = await getUserPhoto();
|
||||||
const userPhoto = await getUserPhoto(accessToken);
|
|
||||||
|
|
||||||
setUserPhoto(userPhoto);
|
// setUserPhoto(userPhoto);
|
||||||
|
|
||||||
appContext.dispatch({
|
// appContext.dispatch({
|
||||||
type: 'SET_USER_PROFILE',
|
// type: 'SET_USER_PROFILE',
|
||||||
payload: userProfile
|
// payload: userProfile
|
||||||
});
|
// });
|
||||||
|
const oidc = await getOidc();
|
||||||
|
|
||||||
|
if (oidc.isUserLoggedIn) {
|
||||||
|
console.log((await oidc.getTokens()).accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -124,13 +124,15 @@ const SiteNav = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isAuthenticated &&
|
isUserLoggedIn &&
|
||||||
Object.keys(appContext.state.userProfile).length === 0
|
Object.keys(appContext.state.userProfile).length === 0
|
||||||
) {
|
) {
|
||||||
|
console.log(isUserLoggedIn)
|
||||||
|
console.log()
|
||||||
setUserProfile();
|
setUserProfile();
|
||||||
getPages();
|
getPages();
|
||||||
}
|
}
|
||||||
}, [isAuthenticated]);
|
}, [isUserLoggedIn]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getPages();
|
getPages();
|
||||||
@@ -138,10 +140,11 @@ const SiteNav = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
|
authenticated={isUserLoggedIn}
|
||||||
|
color='base'
|
||||||
handlePageClick={handlePageClick}
|
handlePageClick={handlePageClick}
|
||||||
handleSignIn={handleSignIn}
|
handleSignIn={handleSignIn}
|
||||||
isAuthenticated={isAuthenticated}
|
logo={<Icon className='mt-1' iconName={IconName.PLANE} size="2x" />}
|
||||||
logo={<Icon iconName={IconName.PLANE} size="2x" />}
|
|
||||||
pages={pages}
|
pages={pages}
|
||||||
settings={<Settings />}
|
settings={<Settings />}
|
||||||
userPhoto={userPhoto}
|
userPhoto={userPhoto}
|
||||||