diff --git a/.DS_Store b/.DS_Store index f3bfd60..e390b1b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/api_build.yaml b/.github/workflows/api_build.yaml deleted file mode 100644 index f76b078..0000000 --- a/.github/workflows/api_build.yaml +++ /dev/null @@ -1,69 +0,0 @@ -name: 'API Build' -on: - workflow_call: - inputs: - environment_name: - required: true - type: string - version_number: - required: true - type: string - -jobs: - build: - runs-on: ubuntu-latest - environment: ${{ inputs.environment_name }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - api - - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false - - - name: Install Nest CLI - run: | - npm install -g @nestjs/cli - - - name: 'Setup Node' - uses: actions/setup-node@v4 - with: - node-version: ${{ vars.NODE_VERSION }} - - - name: Install dependencies - run: | - pnpm install - - - name: Build - run: | - pnpm --filter api build - - - name: Deploy - if: ${{ github.event_name != 'pull_request' }} - run: | - pnpm --filter api --prod deploy ./.prod/api - - - name: Log into Docker Hub - if: ${{ github.event_name != 'pull_request' }} - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Setup Docker Buildx - if: ${{ github.event_name != 'pull_request' }} - uses: docker/setup-buildx-action@v3 - - - name: Build and push to registry - if: ${{ github.event_name != 'pull_request' }} - uses: docker/build-push-action@v6 - with: - push: true - tags: noahspan/flying-api:${{ inputs.version_number }} - context: . - target: api diff --git a/.github/workflows/app_build.yaml b/.github/workflows/app_build.yaml deleted file mode 100644 index 5cc19da..0000000 --- a/.github/workflows/app_build.yaml +++ /dev/null @@ -1,70 +0,0 @@ -name: 'App Build' -on: - workflow_call: - inputs: - environment_name: - required: true - type: string - version_number: - required: true - type: string - -env: - VITE_API_URL: ${{ vars.VITE_API_URL }} - VITE_CLIENT_ID: ${{ vars.VITE_CLIENT_ID }} - VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }} - VITE_REDIRECT_URL: ${{ vars.VITE_REDIRECT_URL }} - -jobs: - build: - runs-on: ubuntu-latest - environment: ${{ inputs.environment_name }} - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: | - app - - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false - - - name: 'Setup Node' - uses: actions/setup-node@v4 - with: - node-version: ${{ vars.NODE_VERSION }} - - - name: Install - run: | - pnpm install - - - name: Build - run: | - pnpm --filter app build - - - name: Deploy - if: ${{ github.event_name != 'pull_request' }} - run: | - pnpm --filter app --prod deploy ./.prod/app - - - name: Setup Docker Buildx - if: ${{ github.event_name != 'pull_request' }} - uses: docker/setup-buildx-action@v3 - - - name: Log into Docker Hub - if: ${{ github.event_name != 'pull_request' }} - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push to registry - if: ${{ github.event_name != 'pull_request' }} - uses: docker/build-push-action@v6 - with: - push: true - tags: noahspan/flying-app:${{ inputs.version_number }} - context: . - target: app diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 40a9e70..0fd1067 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -8,48 +8,25 @@ jobs: changes: uses: ./.github/workflows/changes.yaml - build-api: - if: ${{ needs.changes.outputs.api == 'true' }} - name: build-api - needs: - - changes - uses: ./.github/workflows/api_build.yaml - with: - environment_name: test - version_number: ${{ github.run_id }} - secrets: inherit - - deploy-api: - name: deploy-api - needs: - - changes - - build-api - uses: ./.github/workflows/deploy.yaml - with: - app_name: flying-api - environment_name: test - version_number: ${{ github.run_id }} - secrets: inherit - - build-app: + build: if: ${{ needs.changes.outputs.app == 'true' }} - name: build-app + name: build needs: - changes - uses: ./.github/workflows/app_build.yaml + uses: ./.github/workflows/build.yaml with: environment_name: test version_number: ${{ github.run_id }} secrets: inherit - deploy-app: - name: deploy-app + deploy: + name: deploy needs: - changes - - build-app + - build uses: ./.github/workflows/deploy.yaml with: - app_name: flying-app + app_name: flying environment_name: test version_number: ${{ github.run_id }} secrets: inherit diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index 73277e2..20ff45f 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -5,40 +5,21 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+' jobs: - build-api: - name: build-api - uses: ./.github/workflows/api_build.yaml + build: + name: build + uses: ./.github/workflows/build.yaml with: environment_name: prod version_number: ${{ github.ref_name }} secrets: inherit - deploy-api: - name: deploy-api + deploy: + name: deploy needs: - - build-api + - build uses: ./.github/workflows/deploy.yaml with: - app_name: flying-api - environment_name: prod - version_number: ${{ github.ref_name }} - secrets: inherit - - build-app: - name: build-app - uses: ./.github/workflows/app_build.yaml - with: - environment_name: prod - version_number: ${{ github.ref_name }} - secrets: inherit - - deploy-app: - name: deploy-app - needs: - - build-app - uses: ./.github/workflows/deploy.yaml - with: - app_name: flying-app + app_name: flying environment_name: prod version_number: ${{ github.ref_name }} secrets: inherit diff --git a/app/README.md b/.turbo/daemon/a55a0ce1e1048a13-turbo.log.2025-09-13 similarity index 100% rename from app/README.md rename to .turbo/daemon/a55a0ce1e1048a13-turbo.log.2025-09-13 diff --git a/.turbo/daemon/a55a0ce1e1048a13-turbo.log.2025-09-14 b/.turbo/daemon/a55a0ce1e1048a13-turbo.log.2025-09-14 new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile index 0c41349..4da73dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,18 @@ -# FROM --platform=linux/amd64 node:18-alpine AS base -# ENV PNPM_HOME="/pnpm" -# ENV PATH="$PNPM_HOME:$PATH" -# RUN corepack enable +FROM --platform=linux/amd64 node:22-slim -# FROM base AS api -# COPY /api/dist /app/dist -# COPY /api/node_modules /app/node_modules -# WORKDIR /app -# EXPOSE 3000 -# # CMD ["node", "dist/main.js"] -# ENTRYPOINT ["tail", "-f", "/dev/null"] +WORKDIR app +COPY ./api/dist ./api/dist +COPY ./api/package.json package-lock.json ./api +COPY ./client/dist ./client/dist -# FROM base AS app -# COPY /app/dist /app/dist -# WORKDIR /app -# RUN npm i -g serve -# EXPOSE 8080 -# CMD [ "serve", "-s", "/dist", "-p", "8080" ] - - - - - -FROM node:18-slim AS base - -FROM base AS api WORKDIR api -COPY ./.prod/api . -EXPOSE 3000 -CMD ["node", "dist/main.js"] -# ENTRYPOINT ["tail", "-f", "/dev/null"] +RUN npm ci -FROM base AS app -WORKDIR /app -COPY ./.prod/app . -RUN npm i -g serve -EXPOSE 8080 -CMD [ "serve", "-s", "dist", "-p", "8080" ] \ No newline at end of file +WORKDIR / +COPY ./api/entrypoint.sh ./entrypoint.sh +RUN chmod +x entrypoint.sh + +EXPOSE 3000 + +ENTRYPOINT ["/entrypoint.sh"] +# ENTRYPOINT ["tail", "-f", "/dev/null"] \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 9fc5a6c..8578023 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -57,3 +57,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json local.settings.json + + +/src/database/*.db diff --git a/api/entrypoint.sh b/api/entrypoint.sh new file mode 100644 index 0000000..7eaefbe --- /dev/null +++ b/api/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/bash +npx typeorm migration:run -d ./app/api/dist/database/data-source.js +node ./app/api/dist/main.js \ No newline at end of file diff --git a/api/package.json b/api/package.json index bf020a0..4e51c07 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "api", - "version": "1.4.0", + "version": "2.0.0-alpha", "description": "", "author": "", "private": true, @@ -16,24 +16,40 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "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/database/data-source.js", + "migration:generate": "npm run typeorm -- migration:generate", + "migration:run": "npm run typeorm -- migration:run", + "migration:revert": "npm run typeorm -- migration:revert" }, "dependencies": { "@azure/storage-blob": "^12.27.0", "@microsoft/microsoft-graph-client": "^3.0.7", - "@nestjs/axios": "^3.0.3", - "@nestjs/common": "^10.0.0", - "@nestjs/config": "^3.2.2", - "@nestjs/core": "^10.0.0", - "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.0.0", + "@nestjs/axios": "^4.0.1", + "@nestjs/common": "^11.1.6", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.6", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.6", + "@nestjs/serve-static": "^5.0.3", + "@nestjs/typeorm": "^11.0.0", "@noahspan/azure-database": "^3.1.2", - "@noahspan/noahspan-modules": "^1.1.5", + "@noahspan/noahspan-modules": "^1.2.9", "@schematics/angular": "^17.3.7", "@types/multer": "^1.4.12", - "dotenv": "^16.4.7", + "better-sqlite3": "^12.2.0", + "dotenv": "^16.6.1", + "express-session": "^1.18.2", + "jwks-rsa": "^3.2.0", + "jwt-decode": "^4.0.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", "rxjs": "^7.8.1", + "typeorm": "^0.3.25", "uuid": "^10.0.0", "uuidv4": "^6.2.13" }, @@ -41,11 +57,13 @@ "@microsoft/microsoft-graph-types": "^2.40.0", "@nestjs/cli": "^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-session": "^1.18.2", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/passport-azure-ad": "^4.3.6", + "@types/passport-openidconnect": "^0.1.3", "@types/supertest": "^6.0.0", "jest": "^29.5.0", "source-map-support": "^0.5.21", diff --git a/api/src/app.controller.spec.ts b/api/src/app.controller.spec.ts deleted file mode 100644 index c6e6584..0000000 --- a/api/src/app.controller.spec.ts +++ /dev/null @@ -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); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/api/src/app.module.ts b/api/src/app.module.ts index a4ac52f..c5c179a 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -1,12 +1,17 @@ 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 { PilotModule } from './pilot/pilot.module'; import { APP_FILTER, APP_GUARD, Reflector } from '@nestjs/core'; import { HttpExceptionFilter } from './filters/http-exception.filter'; 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 { TypeOrmModule } from '@nestjs/typeorm'; +import { dataSourceOptions } from './database/data-source'; +import { TrackModule } from './track/track.module'; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { join } from 'path'; @Module({ imports: [ @@ -15,19 +20,25 @@ import configuration from './config/configuration'; imports: [ConfigModule], useFactory: async (configService: ConfigService) => { return { - clientId: configService.get('clientId'), - clientSecret: configService.get('clientSecret'), - tenantId: configService.get('tenantId') + audience: configService.get('audience'), + issuerUrl: configService.get('issuer'), + jwksUri: configService.get('jwksUri') }; }, }), ConfigModule.forRoot({ + isGlobal: true, load: [configuration] }), - FeatureFlagModule, + // HealthModule, LogModule, PilotModule, - UserModule.registerAsync({ + ServeStaticModule.forRoot({ + rootPath: join(__dirname, '../..', 'client', 'dist') + }), + TrackModule, + TypeOrmModule.forRoot(dataSourceOptions), + MsGraphModule.registerAsync({ inject: [ConfigService], imports: [ConfigModule], useFactory: async (configService: ConfigService) => { @@ -43,7 +54,7 @@ import configuration from './config/configuration'; { provide: APP_FILTER, useClass: HttpExceptionFilter - }, + } ] }) export class AppModule {} diff --git a/api/src/auth/auth.interface.ts b/api/src/auth/auth.interface.ts deleted file mode 100644 index b274c37..0000000 --- a/api/src/auth/auth.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface AuthModuleOptions { - tenantId: string; - clientId: string; - clientSecret: string; -} diff --git a/api/src/auth/auth.module-definition.ts b/api/src/auth/auth.module-definition.ts deleted file mode 100644 index 7f69668..0000000 --- a/api/src/auth/auth.module-definition.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ConfigurableModuleBuilder } from '@nestjs/common'; -import { AuthModuleOptions } from './auth.interface'; - -export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder().build() \ No newline at end of file diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts deleted file mode 100644 index 4234bb6..0000000 --- a/api/src/auth/auth.module.ts +++ /dev/null @@ -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 {} diff --git a/api/src/auth/auth.strategy.ts b/api/src/auth/auth.strategy.ts deleted file mode 100644 index cd521a4..0000000 --- a/api/src/auth/auth.strategy.ts +++ /dev/null @@ -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 { - return data; - } -} \ No newline at end of file diff --git a/api/src/certificate/certificate.controller.ts b/api/src/certificate/certificate.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/certificate/certificate.dto.ts b/api/src/certificate/certificate.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/certificate/certificate.entity.ts b/api/src/certificate/certificate.entity.ts new file mode 100644 index 0000000..0a1acbc --- /dev/null +++ b/api/src/certificate/certificate.entity.ts @@ -0,0 +1,21 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { PilotEntity } from '../pilot/pilot.entity'; + +@Entity({ name: 'certificates'}) +export class CertificateEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + type: string; + + @Column() + number: string; + + @Column() + issueDate: Date + + @ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ name: 'pilotId' }) + pilot: PilotEntity; +} \ No newline at end of file diff --git a/api/src/certificate/certificate.module.ts b/api/src/certificate/certificate.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/certificate/certificate.service.ts b/api/src/certificate/certificate.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/config/configuration.ts b/api/src/config/configuration.ts index 2354131..437721c 100644 --- a/api/src/config/configuration.ts +++ b/api/src/config/configuration.ts @@ -1,6 +1,9 @@ export default () => ({ azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, + audience: process.env.AUDIENCE, clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, + issuer: process.env.ISSUER_URL, + jwksUri: process.env.JWKS_URI, tenantId: process.env.TENANT_ID }) \ No newline at end of file diff --git a/api/src/database/data-source.ts b/api/src/database/data-source.ts new file mode 100644 index 0000000..ee34a70 --- /dev/null +++ b/api/src/database/data-source.ts @@ -0,0 +1,19 @@ +import { DataSource, DataSourceOptions } from 'typeorm'; +import { config } from 'dotenv'; +import { ConfigService } from '@nestjs/config'; + +config(); + +const configService = new ConfigService(); + +export const dataSourceOptions: DataSourceOptions = { + type: 'better-sqlite3', + database: configService.get('DB_PATH'), + entities: ['../**/*.entity.js'], + migrations: ['./migrations/*.js'], + synchronize: configService.get('DB_SYNC') +} + +const dataSource = new DataSource(dataSourceOptions); + +export default dataSource; \ No newline at end of file diff --git a/api/src/database/litestream.yml b/api/src/database/litestream.yml new file mode 100644 index 0000000..6c5a52b --- /dev/null +++ b/api/src/database/litestream.yml @@ -0,0 +1,4 @@ +dbs: + - path: /var/lib/data/flying.db + replicas: + - path: /mnt/data/backup \ No newline at end of file diff --git a/api/src/database/migrations/1758802917932-initial_migration.ts b/api/src/database/migrations/1758802917932-initial_migration.ts new file mode 100644 index 0000000..5acba6f --- /dev/null +++ b/api/src/database/migrations/1758802917932-initial_migration.ts @@ -0,0 +1,64 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InitialMigration1758802917932 implements MigrationInterface { + name = 'InitialMigration1758802917932' + + public async up(queryRunner: QueryRunner): Promise { + 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 "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, "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 "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(`INSERT INTO "temporary_certificates"("id", "type", "number", "issueDate", "pilotId") SELECT "id", "type", "number", "issueDate", "pilotId" FROM "certificates"`); + await queryRunner.query(`DROP TABLE "certificates"`); + await queryRunner.query(`ALTER TABLE "temporary_certificates" RENAME TO "certificates"`); + await queryRunner.query(`CREATE TABLE "temporary_endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_96a69a0bfbdeccce6bffe34002d" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_endorsements"("id", "type", "issueDate", "pilotId") SELECT "id", "type", "issueDate", "pilotId" FROM "endorsements"`); + await queryRunner.query(`DROP TABLE "endorsements"`); + await queryRunner.query(`ALTER TABLE "temporary_endorsements" RENAME TO "endorsements"`); + await queryRunner.query(`CREATE TABLE "temporary_medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_cb1f5d88fa2b105cc77513e9082" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_medical"("id", "class", "expirationDate", "pilotId") SELECT "id", "class", "expirationDate", "pilotId" FROM "medical"`); + await queryRunner.query(`DROP TABLE "medical"`); + await queryRunner.query(`ALTER TABLE "temporary_medical" RENAME TO "medical"`); + await queryRunner.query(`CREATE TABLE "temporary_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, CONSTRAINT "FK_19598551658f7625a82c7f029c8" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_logs"("id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId") SELECT "id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId" FROM "logs"`); + await queryRunner.query(`DROP TABLE "logs"`); + await queryRunner.query(`ALTER TABLE "temporary_logs" RENAME TO "logs"`); + await queryRunner.query(`CREATE TABLE "temporary_tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar, CONSTRAINT "FK_71881df31cfff2362e39accc4b2" FOREIGN KEY ("logId") REFERENCES "logs" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_tracks"("id", "url", "order", "logId") SELECT "id", "url", "order", "logId" FROM "tracks"`); + await queryRunner.query(`DROP TABLE "tracks"`); + await queryRunner.query(`ALTER TABLE "temporary_tracks" RENAME TO "tracks"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tracks" RENAME TO "temporary_tracks"`); + 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(`INSERT INTO "tracks"("id", "url", "order", "logId") SELECT "id", "url", "order", "logId" FROM "temporary_tracks"`); + await queryRunner.query(`DROP TABLE "temporary_tracks"`); + await queryRunner.query(`ALTER TABLE "logs" RENAME TO "temporary_logs"`); + 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(`INSERT INTO "logs"("id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId") SELECT "id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId" FROM "temporary_logs"`); + await queryRunner.query(`DROP TABLE "temporary_logs"`); + await queryRunner.query(`ALTER TABLE "medical" RENAME TO "temporary_medical"`); + 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(`INSERT INTO "medical"("id", "class", "expirationDate", "pilotId") SELECT "id", "class", "expirationDate", "pilotId" FROM "temporary_medical"`); + await queryRunner.query(`DROP TABLE "temporary_medical"`); + await queryRunner.query(`ALTER TABLE "endorsements" RENAME TO "temporary_endorsements"`); + 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(`INSERT INTO "endorsements"("id", "type", "issueDate", "pilotId") SELECT "id", "type", "issueDate", "pilotId" FROM "temporary_endorsements"`); + await queryRunner.query(`DROP TABLE "temporary_endorsements"`); + await queryRunner.query(`ALTER TABLE "certificates" RENAME TO "temporary_certificates"`); + 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(`INSERT INTO "certificates"("id", "type", "number", "issueDate", "pilotId") SELECT "id", "type", "number", "issueDate", "pilotId" FROM "temporary_certificates"`); + await queryRunner.query(`DROP TABLE "temporary_certificates"`); + await queryRunner.query(`DROP TABLE "tracks"`); + await queryRunner.query(`DROP TABLE "logs"`); + await queryRunner.query(`DROP TABLE "pilots"`); + await queryRunner.query(`DROP TABLE "medical"`); + await queryRunner.query(`DROP TABLE "endorsements"`); + await queryRunner.query(`DROP TABLE "certificates"`); + } + +} diff --git a/api/src/endorsement/endorsement.controller.ts b/api/src/endorsement/endorsement.controller.ts new file mode 100644 index 0000000..0b1712b --- /dev/null +++ b/api/src/endorsement/endorsement.controller.ts @@ -0,0 +1,65 @@ +import { Body, Controller, Delete, Get, HttpException, Param, Post, Put } from "@nestjs/common"; +import { EndorsementService } from "./endorsement.service"; +import { CustomError } from "@noahspan/noahspan-modules"; +import { EndorsementDto } from "./endorsement.dto"; + + +@Controller('endorsements') +export class EndorsementController { + constructor(private readonly endorsementService: EndorsementService) {} + + @Get(':id') + async find(@Param('id') id: string) { + try { + return await this.endorsementService.find(id); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Get() + async findAll() { + try { + return await this.endorsementService.findAll(); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Post() + async create(@Body() endorsementDto: EndorsementDto) { + try { + return await this.endorsementService.create(endorsementDto); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Put(':id') + async update(@Param('id') id: string, @Body() endorsementDto: EndorsementDto) { + try { + return await this.endorsementService.update(id, endorsementDto); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode) + } + } + + @Delete(':id') + async delete(@Param('id') id: string) { + try { + return await this.endorsementService.delete(id); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode) + } + } +} \ No newline at end of file diff --git a/api/src/endorsement/endorsement.dto.ts b/api/src/endorsement/endorsement.dto.ts new file mode 100644 index 0000000..7332afa --- /dev/null +++ b/api/src/endorsement/endorsement.dto.ts @@ -0,0 +1,3 @@ +export class EndorsementDto { + +} \ No newline at end of file diff --git a/api/src/endorsement/endorsement.entity.ts b/api/src/endorsement/endorsement.entity.ts new file mode 100644 index 0000000..240e40b --- /dev/null +++ b/api/src/endorsement/endorsement.entity.ts @@ -0,0 +1,18 @@ +import { PilotEntity } from 'src/pilot/pilot.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; + +@Entity({ name: 'endorsements' }) +export class EndorsementEntity { + @PrimaryGeneratedColumn('uuid') + id: string + + @Column() + type: string; + + @Column() + issueDate: Date; + + @ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ name: 'pilotId' }) + pilot: PilotEntity +} \ No newline at end of file diff --git a/api/src/endorsement/endorsement.module.ts b/api/src/endorsement/endorsement.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/endorsement/endorsement.service.ts b/api/src/endorsement/endorsement.service.ts new file mode 100644 index 0000000..16cb895 --- /dev/null +++ b/api/src/endorsement/endorsement.service.ts @@ -0,0 +1,32 @@ +import { InjectRepository } from "@nestjs/typeorm"; +import { EndorsementEntity } from "./endorsement.entity"; +import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm'; +import { Injectable } from "@nestjs/common"; +import { EndorsementDto } from "./endorsement.dto"; + +@Injectable() +export class EndorsementService { + constructor( + @InjectRepository(EndorsementEntity) private readonly endorsementRepository: Repository + ) {} + + async find(id: string): Promise { + return await this.endorsementRepository.findOneBy({ id }); + } + + async findAll(): Promise { + return await this.endorsementRepository.find(); + } + + async create(endorsement: EndorsementDto): Promise { + return await this.endorsementRepository.insert(endorsement); + } + + async update(id: string, endorsement: EndorsementDto): Promise { + return await this.endorsementRepository.update(id, endorsement); + } + + async delete(id: string): Promise { + return await this.endorsementRepository.delete({ id }); + } +} \ No newline at end of file diff --git a/api/src/featureFlag/feature-flag.controller.ts b/api/src/featureFlag/feature-flag.controller.ts deleted file mode 100644 index d8eb19d..0000000 --- a/api/src/featureFlag/feature-flag.controller.ts +++ /dev/null @@ -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); - } - } -} diff --git a/api/src/featureFlag/feature-flag.dto.ts b/api/src/featureFlag/feature-flag.dto.ts deleted file mode 100644 index 0b31d35..0000000 --- a/api/src/featureFlag/feature-flag.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class FeatureFlagDto { - partitionKey: string; - rowKey: string; - active: string; -} diff --git a/api/src/featureFlag/feature-flag.entity.ts b/api/src/featureFlag/feature-flag.entity.ts deleted file mode 100644 index d5a4e4c..0000000 --- a/api/src/featureFlag/feature-flag.entity.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { EntityString } from '@noahspan/azure-database'; - -export class FeatureFlag { - @EntityString() partitionKey: string; - @EntityString() rowKey: string; - @EntityString() active: string; -} diff --git a/api/src/featureFlag/feature-flag.module.ts b/api/src/featureFlag/feature-flag.module.ts deleted file mode 100644 index bc39292..0000000 --- a/api/src/featureFlag/feature-flag.module.ts +++ /dev/null @@ -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('azureStorageConnectionString') - }; - }, - inject: [ConfigService] - }), - AzureTableStorageModule.forFeature(FeatureFlag, { - createTableIfNotExists: false, - table: 'featureFlags' - }), - ], - controllers: [FeatureFlagController], - providers: [FeatureFlagService] -}) -export class FeatureFlagModule {} diff --git a/api/src/featureFlag/feature-flag.service.ts b/api/src/featureFlag/feature-flag.service.ts deleted file mode 100644 index b05dbb0..0000000 --- a/api/src/featureFlag/feature-flag.service.ts +++ /dev/null @@ -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 - ) {} - - async find(partitionKey: string, rowKey: string): Promise { - return await this.featureFlagRepository.find(partitionKey, rowKey); - } - - async findAll(): Promise { - return await this.featureFlagRepository.findAll(); - } -} diff --git a/api/src/file/file.service.ts b/api/src/file/file.service.ts index 163cd0d..3c82e58 100644 --- a/api/src/file/file.service.ts +++ b/api/src/file/file.service.ts @@ -37,10 +37,10 @@ import { ConfigService } from '@nestjs/config'; return blockBlobClient; } - async uploadFile(file: Express.Multer.File, containerName: string, rowKey: string): Promise { + async uploadFile(file: Express.Multer.File, containerName: string, logId: string): Promise { this.containerName = containerName; - const blockBlobClient = await this.getBlobClient(`${rowKey}/${file.originalname}`); + const blockBlobClient = await this.getBlobClient(`${logId}/${file.originalname}`); const fileUrl = blockBlobClient.url; await blockBlobClient.uploadData(file.buffer); @@ -58,11 +58,32 @@ import { ConfigService } from '@nestjs/config'; return downloaded } - async deleteFile(containerName: string, rowKey:string, fileName: string): Promise { + async deleteFile(containerName: string, logId: string, fileName: string): Promise { this.containerName = containerName; - const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`); + const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`); await blockBlobClient.deleteIfExists(); } + + async deleteFolder(containerName: string, logId: string): Promise { + const blobService = await this.getBlobServiceInstance(); + + this.containerName = containerName; + + const containerClient = blobService.getContainerClient(containerName); + const blobsToDelete = [] + + for await (const blob of containerClient.listBlobsFlat({ prefix: logId })) { + blobsToDelete.push(blob.name) + } + + for (const blobName of blobsToDelete) { + const blockBlobClient = containerClient.getBlockBlobClient(blobName); + + await blockBlobClient.delete(); + } + + return; + } } \ No newline at end of file diff --git a/api/src/health/health.controller.spec.ts b/api/src/health/health.controller.spec.ts new file mode 100644 index 0000000..c2bf1be --- /dev/null +++ b/api/src/health/health.controller.spec.ts @@ -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); + }) + + 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); + }) +}) \ No newline at end of file diff --git a/api/src/health/health.controller.ts b/api/src/health/health.controller.ts new file mode 100644 index 0000000..6a0d33e --- /dev/null +++ b/api/src/health/health.controller.ts @@ -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 { + try { + return await this.healthService.isDatabaseConnected(); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } +} diff --git a/api/src/health/health.module.ts b/api/src/health/health.module.ts new file mode 100644 index 0000000..15a913d --- /dev/null +++ b/api/src/health/health.module.ts @@ -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 {} diff --git a/api/src/health/health.service.spec.ts b/api/src/health/health.service.spec.ts new file mode 100644 index 0000000..a3d3f85 --- /dev/null +++ b/api/src/health/health.service.spec.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/api/src/health/health.service.ts b/api/src/health/health.service.ts new file mode 100644 index 0000000..53ec80b --- /dev/null +++ b/api/src/health/health.service.ts @@ -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 { + 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; + } + } +} diff --git a/api/src/interfaces/customJwtPayload.interface.ts b/api/src/interfaces/customJwtPayload.interface.ts new file mode 100644 index 0000000..17b3c82 --- /dev/null +++ b/api/src/interfaces/customJwtPayload.interface.ts @@ -0,0 +1,5 @@ +import { JwtPayload } from "jwt-decode"; + +export interface CustomJwtPayload extends JwtPayload { + roles: string[]; +} \ No newline at end of file diff --git a/api/src/log/interceptors/log.interceptor.ts b/api/src/log/interceptors/log.interceptor.ts deleted file mode 100644 index ef95f79..0000000 --- a/api/src/log/interceptors/log.interceptor.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; -import { Observable, map } from 'rxjs'; - -export class LogInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, handler: CallHandler): Observable { - const req = context.switchToHttp().getRequest(); - const authHeader = req.headers.authorization; - const token = authHeader && authHeader.split(' ')[1]; - console.log(token) - - if (!token) { - return handler.handle().pipe( - map((data) => { - if (data.length) { - const logs = data.map((log) => { - return { - partitionKey: log.partitionKey, - rowKey: log.rowKey, - pilotId: log.pilotId, - pilotName: log.pilotName, - date: log.date, - aircraftMakeModel: log.aircraftMakeModel, - routeFrom: log.routeFrom, - routeTo: log.routeTo, - durationOfFlight: log.durationOfFlight, - tracks: log.tracks, - notes: log.notes - }; - }); - - return logs; - } else { - return data; - } - }) - ); - } - - return handler.handle().pipe(map((data) => data)); - } -} diff --git a/api/src/log/log.controller.ts b/api/src/log/log.controller.ts index 773c0cd..702aea1 100644 --- a/api/src/log/log.controller.ts +++ b/api/src/log/log.controller.ts @@ -7,22 +7,23 @@ import { Param, Post, Put, - Query, - StreamableFile, - UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { LogDto } from './log.dto'; -import { Log } from './log.entity'; +import { LogEntity } from './log.entity'; import { LogService } from './log.service'; import { CustomError } from '../error/customError'; -import { AuthGuard } from '@noahspan/noahspan-modules'; -import { LogInterceptor } from './interceptors/log.interceptor'; +import { AuthGuard, Public } from '@noahspan/noahspan-modules'; +import { LogInterceptor } from './log.interceptor'; import { FileService } from '../file/file.service'; -import { FileInterceptor } from '@nestjs/platform-express'; +import { DeleteResult, InsertResult, UpdateResult } from 'typeorm'; +import { Reflector } from '@nestjs/core'; + +const reflector = new Reflector(); @Controller('logs') +@UseInterceptors(new LogInterceptor(reflector)) export class LogController { constructor( private readonly fileService: FileService, @@ -30,116 +31,73 @@ export class LogController { ) {} - @Get(':partitionKey/:rowKey') - @UseInterceptors(new LogInterceptor()) + @Get(':id') + @Public() async find( - @Param('partitionKey') partitionKey: string, - @Param('rowKey') rowKey: string - ): Promise { + @Param('id') id: string, + ): Promise { try { - return await this.logService.find(partitionKey, rowKey); + console.log(id) + return await this.logService.find(id); } catch (error) { const customError = error as CustomError; - + console.log(error) throw new HttpException(customError.message, customError.statusCode); } } @Get() - @UseInterceptors(new LogInterceptor()) - async findAll(): Promise { + @Public() + async findAll(): Promise { try { + console.log('blah') return await this.logService.findAll(); } catch (error) { const customError = error as CustomError; - + console.log(error) throw new HttpException(customError.message, customError.statusCode); } } - @UseGuards(AuthGuard) - @Post() - async create(@Body() logDto: LogDto): Promise { - try { - const log = new Log(); - - Object.assign(log, logDto); - - return await this.logService.create(log); - } catch (error) { - const customError = error as CustomError; - - throw new HttpException(customError.message, customError.statusCode); - } - } - - @UseGuards(AuthGuard) - @Put(':partitionKey/:rowKey') - async update( - @Param('partitionKey') partitionKey: string, - @Param('rowKey') rowKey: string, - @Body() logDto: LogDto - ): Promise { - try { - const log = new Log(); - - Object.assign(log, logDto); - - return await this.logService.update(partitionKey, rowKey, log); - } catch (error) { - const customError = error as CustomError; - - throw new HttpException(customError.message, customError.statusCode); - } - } - - @UseGuards(AuthGuard) - @Delete(':partitionKey/:rowKey') - async delete( - @Param('partitionKey') partitionKey: string, - @Param('rowKey') rowKey: string - ): Promise { - try { - return await this.logService.delete(partitionKey, rowKey); - } catch (error) { - const customError = error as CustomError; - - throw new HttpException(customError.message, customError.statusCode); - } - } - - @UseGuards(AuthGuard) - @Post(':partitionKey/:rowKey/track') - @UseInterceptors(FileInterceptor('file')) - async createTrack(@Param('rowKey') rowKey: string, @UploadedFile() file: Express.Multer.File) { - try { - const containerName = 'tracks'; - const url = await this.fileService.uploadFile(file, containerName, rowKey); - return { url } + @Post() + @UseGuards(AuthGuard) + async create(@Body() logDto: LogDto): Promise { + try { + return await this.logService.create(logDto); } catch (error) { const customError = error as CustomError; - + console.log(error) throw new HttpException(customError.message, customError.statusCode); } } - @Get(':partitionKey/:rowKey/track') - async downloadTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise { - const containerName = 'tracks'; - const downloadedFile: string = await this.fileService.downloadFile(containerName, rowKey, fileName) - - return downloadedFile; + @Put(':id') + @UseGuards(AuthGuard) + async update( + @Param('id') id: string, + @Body() logDto: LogDto + ): Promise { + try { + console.log(id) + console.log(logDto) + return await this.logService.update(id, logDto); + } catch (error) { + const customError = error as CustomError; + console.log(error) + throw new HttpException(customError.message, customError.statusCode); + } } + @Delete(':id') @UseGuards(AuthGuard) - @Delete(':partitionKey/:rowKey/track') - async deleteTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise { + async delete( + @Param('id') id: string, + ): Promise { try { - const containerName = 'tracks'; - - return await this.fileService.deleteFile(containerName, rowKey, fileName) + return await this.logService.delete(id); } catch (error) { + console.log(error) const customError = error as CustomError; throw new HttpException(customError.message, customError.statusCode); diff --git a/api/src/log/log.dto.ts b/api/src/log/log.dto.ts index 9d82968..561984f 100644 --- a/api/src/log/log.dto.ts +++ b/api/src/log/log.dto.ts @@ -1,27 +1,28 @@ +import { PilotEntity } from "src/pilot/pilot.entity"; + export class LogDto { pilotId: string; - pilotName: string; - date: string; + date: Date; aircraftMakeModel: string; aircraftIdentity: string; routeFrom: string; routeTo: string; - durationOfFlight: number; - singleEngineLand: string; - simulatorAtd: number; - landingsDay: number; - landingsNight: number; - instrumentActual: number; - instrumentSimulated: number; - instrumentApproaches: number; - instrumentHolds: number; - instrumentNavTrack: number; - groundTrainingReceived: number; - flightTrainingReceived: number; - crossCountry: number; - night: number; - solo: number; - pilotInCommand: number; - tracks: string[]; - notes: string; + durationOfFlight?: number; + singleEngineLand?: number; + simulatorAtd?: number; + landingsDay?: number; + landingsNight?: number; + instrumentActual?: number; + instrumentSimulated?: number; + instrumentApproaches?: number; + instrumentHolds?: number; + instrumentNavTrack?: number; + groundTrainingReceived?: number; + flightTrainingReceived?: number; + crossCountry?: number; + night?: number; + solo?: number; + pilotInCommand?: number; + notes?: string; + tracks?: [] } diff --git a/api/src/log/log.entity.ts b/api/src/log/log.entity.ts index a0f9179..4c9fe24 100644 --- a/api/src/log/log.entity.ts +++ b/api/src/log/log.entity.ts @@ -1,29 +1,85 @@ -export class Log { - partitionKey: string; - rowKey: string; +import { PilotEntity } from 'src/pilot/pilot.entity'; +import { TrackEntity } from 'src/track/track.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm'; + +@Entity({ name: 'logs' }) +export class LogEntity { + @PrimaryGeneratedColumn('uuid') + id: string + + @Column() pilotId: string; - pilotName: string; - date: string; + + @Column() + date: Date; + + @Column() aircraftMakeModel: string; + + @Column() aircraftIdentity: string; + + @Column() routeFrom: string; + + @Column() routeTo: string; - durationOfFlight: number | null; + + @Column() + durationOfFlight: number; + + @Column({ nullable: true }) 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; -} + + @Column({ nullable: true }) + simulatorAtd: number | null; + + @Column({ nullable: true }) + landingsDay: number | null; + + @Column({ nullable: true }) + landingsNight: number | null; + + @Column({ nullable: true }) + groundTrainingReceived: number | null; + + @Column({ nullable: true }) + flightTrainingReceived: number | null; + + @Column({ nullable: true }) + crossCountry: number | null; + + @Column({ nullable: true }) + night: number | null; + + @Column({ nullable: true }) + solo: number | null; + + @Column({ nullable: true }) + pilotInCommand: number | null; + + @Column({ nullable: true }) + instrumentActual: number | null; + + @Column({ nullable: true }) + instrumentSimulated: number | null; + + @Column({ nullable: true }) + instrumentApproaches: number | null; + + @Column({ nullable: true }) + instrumentHolds: number | null; + + @Column({ nullable: true }) + instrumentNavTrack: number | null; + + @Column({ nullable: true }) + notes: string | null; + + @OneToMany(() => TrackEntity, (track: TrackEntity) => track.log) + tracks: TrackEntity[] + + @ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs, {onDelete: 'CASCADE', onUpdate: 'CASCADE'}) + @JoinColumn({ name: 'pilotId' }) + pilot: PilotEntity; +} \ No newline at end of file diff --git a/api/src/log/log.interceptor.ts b/api/src/log/log.interceptor.ts new file mode 100644 index 0000000..15676dc --- /dev/null +++ b/api/src/log/log.interceptor.ts @@ -0,0 +1,59 @@ +import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common'; +import { Observable, map } from 'rxjs'; +import { LogEntity } from './log.entity'; +import { jwtDecode } from 'jwt-decode'; +import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface'; +import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules'; +import { Reflector } from '@nestjs/core'; + +export class LogInterceptor implements NestInterceptor { + constructor(private reflector: Reflector) {} + + intercept(context: ExecutionContext, handler: CallHandler): Observable { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + return handler.handle().pipe( + map((data: LogEntity[]) => { + const req = context.switchToHttp().getRequest(); + const limitData = (data) => { + return data.map((log: LogEntity) => { + return { + id: log.id, + pilot: { + name: log.pilot.name + }, + date: log.date, + aircraftMakeModel: log.aircraftMakeModel, + routeFrom: log.routeFrom, + routeTo: log.routeTo, + durationOfFlight: log.durationOfFlight, + tracks: log.tracks, + }; + }); + } + + if (req.headers.authorization) { + const authHeader = req.headers.authorization; + const token = authHeader && authHeader.split(' ')[1]; + const jwtPayload: CustomJwtPayload = jwtDecode(token); + + if (jwtPayload.roles.includes('Flying.Read')) { + const logs = limitData(data); + + return logs; + } else { + return data; + } + } else if (!req.headers.authorization && isPublic) { + const publicData = limitData(data) + const logs = publicData.slice(0, 5) + + return logs; + } + }) + ); + } +} diff --git a/api/src/log/log.module.ts b/api/src/log/log.module.ts index 111ada4..b2a93af 100644 --- a/api/src/log/log.module.ts +++ b/api/src/log/log.module.ts @@ -1,28 +1,19 @@ import { Module } from '@nestjs/common'; import { LogController } from './log.controller'; import { LogService } from './log.service'; -import { AzureTableStorageModule } from '@noahspan/azure-database'; -import { Log } from './log.entity'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { LogEntity } from './log.entity'; +import { ConfigService } from '@nestjs/config'; import { FileService } from '../file/file.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PilotModule } from 'src/pilot/pilot.module'; @Module({ imports: [ - AzureTableStorageModule.forRootAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => { - return { - connectionString: configService.get('azureStorageConnectionString') - }; - }, - inject: [ConfigService] - }), - AzureTableStorageModule.forFeature(Log, { - createTableIfNotExists: false, - table: 'logs' - }), + PilotModule, + TypeOrmModule.forFeature([LogEntity]) ], controllers: [LogController], + exports: [LogService], providers: [ ConfigService, FileService, diff --git a/api/src/log/log.service.ts b/api/src/log/log.service.ts index c7de866..ba46213 100644 --- a/api/src/log/log.service.ts +++ b/api/src/log/log.service.ts @@ -1,34 +1,63 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository, Repository } from '@noahspan/azure-database'; -import { Log } from './log.entity'; -import { v4 as uuidv4 } from 'uuid'; +import { InjectRepository } from '@nestjs/typeorm'; +import { LogEntity } from './log.entity'; +import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm'; +import { LogDto } from './log.dto'; +import { PilotService } from 'src/pilot/pilot.service'; +import { PilotEntity } from 'src/pilot/pilot.entity'; +import { CustomError } from 'src/error/customError'; +import { FileService } from 'src/file/file.service'; @Injectable() export class LogService { constructor( - @InjectRepository(Log) private readonly logRepository: Repository + @InjectRepository(LogEntity) private readonly logRepository: Repository, + private readonly fileService: FileService, + private readonly pilotService: PilotService, ) {} - async find(partitionKey: string, rowKey: string): Promise { - return await this.logRepository.find(partitionKey, rowKey); + async find(id: string): Promise { + const logEntity: LogEntity = await this.logRepository.findOne({ + where: { id: id }, + relations: ['pilot', 'tracks'] + }); + + return logEntity; } - async findAll(): Promise { - return await this.logRepository.findAll(); + async findAll(): Promise { + return await this.logRepository.find({ + relations: ['pilot', 'tracks'] + }); } - async create(log: Log): Promise { - log.partitionKey = 'log'; - log.rowKey = uuidv4(); + async create(logDto: LogDto): Promise { + try{ + const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId); - return await this.logRepository.create(log); + if (pilotEntity) { + const { pilotId, ...newLogDto } = logDto; + const log = this.logRepository.create({ + ...newLogDto, + pilot: pilotEntity + }) + + return this.logRepository.insert(log); + } else { + throw new CustomError('Pilot not found', 'Not found', 404); + } + } catch (error) { + throw error + } } - async update(partitionKey: string, rowKey: string, log: Log): Promise { - return await this.logRepository.update(partitionKey, rowKey, log); + async update(id: string, log: LogDto): Promise { + return await this.logRepository.update(id, log); } - async delete(partitionKey: string, rowKey: string): Promise { - await this.logRepository.delete(partitionKey, rowKey); + async delete(id: string): Promise { + await this.fileService.deleteFolder('tracks', id); + + return await this.logRepository.delete({ id }); } } diff --git a/api/src/main.ts b/api/src/main.ts index 3b8b89f..d3968c6 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -3,15 +3,26 @@ import { AppModule } from './app.module'; import { HttpService } from '@nestjs/axios'; import { HttpExceptionFilter } from './filters/http-exception.filter'; import { InternalServerErrorException } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; +import * as session from 'express-session'; async function bootstrap() { const httpService = new HttpService(); 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.useGlobalFilters(new HttpExceptionFilter()); + app.use( + session({ + secret: 'blah', + resave: false, + saveUninitialized: false + }) + ) httpService.axiosRef.interceptors.response.use( (response) => { diff --git a/api/src/medical/medical.controller.ts b/api/src/medical/medical.controller.ts new file mode 100644 index 0000000..25910f9 --- /dev/null +++ b/api/src/medical/medical.controller.ts @@ -0,0 +1,71 @@ +import { Body, Controller, Delete, Get, HttpException, Param, Post, Put, UseGuards } from "@nestjs/common"; +import { MedicalService } from "./medical.service"; +import { CustomError } from "src/error/customError"; +import { MedicalDto } from "./medical.dto"; +import { AuthGuard } from "@noahspan/noahspan-modules"; + + +@Controller('medical') +export class MedicalController { + constructor(private readonly medicalService: MedicalService) {} + + @UseGuards(AuthGuard) + @Get(':id') + async find(@Param('id') id: string) { + try { + return await this.medicalService.find(id) + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @UseGuards(AuthGuard) + @Get() + async findAll() { + try { + return await this.medicalService.findAll(); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @UseGuards(AuthGuard) + @Post() + async create(@Body() medicalDto: MedicalDto) { + try { + return await this.medicalService.create(medicalDto); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @UseGuards(AuthGuard) + @Put(':id') + async update(@Param('id') id: string, @Body() medicalDto: MedicalDto) { + try { + return await this.medicalService.update(id, medicalDto); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @UseGuards(AuthGuard) + @Delete(':id') + async delete(@Param('id') id: string) { + try { + return await this.medicalService.delete(id); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } +} \ No newline at end of file diff --git a/api/src/medical/medical.dto.ts b/api/src/medical/medical.dto.ts new file mode 100644 index 0000000..b63c9a2 --- /dev/null +++ b/api/src/medical/medical.dto.ts @@ -0,0 +1,6 @@ +export class MedicalDto { + id: string; + class: string; + expirationDate: string; + pilotId: string; +} \ No newline at end of file diff --git a/api/src/medical/medical.entity.ts b/api/src/medical/medical.entity.ts new file mode 100644 index 0000000..02550fc --- /dev/null +++ b/api/src/medical/medical.entity.ts @@ -0,0 +1,18 @@ +import { PilotEntity } from 'src/pilot/pilot.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; + +@Entity({ name: 'medical' }) +export class MedicalEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + class: string; + + @Column() + expirationDate: Date; + + @ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ name: 'pilotId' }) + pilot: PilotEntity; +} \ No newline at end of file diff --git a/api/src/medical/medical.module.ts b/api/src/medical/medical.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/medical/medical.service.ts b/api/src/medical/medical.service.ts new file mode 100644 index 0000000..d2a5d36 --- /dev/null +++ b/api/src/medical/medical.service.ts @@ -0,0 +1,33 @@ + +import { MedicalEntity } from "./medical.entity"; +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm'; +import { MedicalDto } from "./medical.dto"; + +@Injectable() +export class MedicalService { + constructor( + @InjectRepository(MedicalEntity) private readonly medicalRepository: Repository + ) {} + + async find(id: string): Promise { + return await this.medicalRepository.findOneBy({ id }); + } + + async findAll(): Promise { + return await this.medicalRepository.find(); + } + + async create(medical: MedicalDto): Promise { + return await this.medicalRepository.insert(medical) + } + + async update(id: string, medical: MedicalDto): Promise { + return await this.medicalRepository.update(id, medical); + } + + async delete(id: string): Promise { + return await this.medicalRepository.delete({ id }); + } +} \ No newline at end of file diff --git a/api/src/pilot/certificate/certificate.entity.ts b/api/src/pilot/certificate/certificate.entity.ts deleted file mode 100644 index 3c2c143..0000000 --- a/api/src/pilot/certificate/certificate.entity.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class Certificate { - type: string; - issueDate: string; - number: string; -} diff --git a/api/src/pilot/endorsement/endorsement.entity.ts b/api/src/pilot/endorsement/endorsement.entity.ts deleted file mode 100644 index f3349e4..0000000 --- a/api/src/pilot/endorsement/endorsement.entity.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class Endorsement { - type: string; - issueDate: Date; -} diff --git a/api/src/pilot/interceptors/pilot.interceptor.ts b/api/src/pilot/interceptors/pilot.interceptor.ts deleted file mode 100644 index 6089ccb..0000000 --- a/api/src/pilot/interceptors/pilot.interceptor.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; -import { Observable, map } from 'rxjs'; - -export class PilotInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, handler: CallHandler): Observable { - const req = context.switchToHttp().getRequest(); - const authHeader = req.headers.authorization; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - return handler.handle().pipe( - map((data) => { - if (data.length) { - const pilots = data.map((pilot) => { - return { - partitionKey: pilot.partitionKey, - rowKey: pilot.rowKey, - id: pilot.id, - name: pilot.name, - certificates: pilot.certificates, - endorsements: pilot.endorsements - }; - }); - - return pilots; - } else { - return data; - } - }) - ); - } - - return handler.handle().pipe(map((data) => data)); - } -} diff --git a/api/src/pilot/pilot.controller.ts b/api/src/pilot/pilot.controller.ts index a143830..3ecc715 100644 --- a/api/src/pilot/pilot.controller.ts +++ b/api/src/pilot/pilot.controller.ts @@ -11,24 +11,24 @@ import { UseInterceptors, } from '@nestjs/common'; import { PilotDto } from './pilot.dto'; -import { Pilot } from './pilot.entity'; import { PilotService } from './pilot.service'; import { CustomError } from '../error/customError'; -import { AuthGuard } from '@noahspan/noahspan-modules' -import { PilotInterceptor } from './interceptors/pilot.interceptor'; +import { PilotInterceptor } from './pilot.interceptor'; +import { AuthGuard, Public } from '@noahspan/noahspan-modules'; +import { Reflector } from '@nestjs/core'; + +const reflector = new Reflector(); @Controller('pilots') -@UseInterceptors(new PilotInterceptor()) +@UseInterceptors(new PilotInterceptor(reflector)) export class PilotController { constructor(private readonly pilotService: PilotService) {} - @Get(':partitionKey/:rowKey') - async find( - @Param('partitionKey') partitionKey: string, - @Param('rowKey') rowKey: string - ) { + @Get(':id') + @Public() + async find(@Param('id') id: string) { try { - return await this.pilotService.find(partitionKey, rowKey); + return await this.pilotService.find(id); } catch (error) { const customError = error as CustomError; @@ -37,75 +37,39 @@ export class PilotController { } @Get() + @Public() async findAll() { try { return await this.pilotService.findAll(); } catch (error) { const customError = error as CustomError; - + console.log(error) throw new HttpException(customError.message, customError.statusCode); } } - @UseGuards(AuthGuard) @Post() + @UseGuards(AuthGuard) async create(@Body() pilotDto: PilotDto) { try { - let pilot = new Pilot(); - pilot = { - partitionKey: pilotDto.partitionKey, - rowKey: pilotDto.rowKey, - id: pilotDto.id, - name: pilotDto.name, - address: pilotDto.address, - city: pilotDto.city, - state: pilotDto.state, - postalCode: pilotDto.postalCode, - email: pilotDto.email, - phone: pilotDto.phone, - medicalClass: pilotDto.medicalClass, - medicalExpiration: pilotDto.medicalExpiration, - certificates: JSON.stringify(pilotDto.certificates), - endorsements: JSON.stringify(pilotDto.endorsements) - } - - return await this.pilotService.create(pilot); + return await this.pilotService.create(pilotDto); } catch (error) { - const customError = error as CustomError; + console.log(error) + // const customError = error as CustomError; - throw new HttpException(customError.message, customError.statusCode); + // throw new HttpException(customError.message, customError.statusCode); } } + @Put(':id') @UseGuards(AuthGuard) - @Put(':partitionKey/:rowKey') async update( - @Param('partitionKey') partitionKey: string, - @Param('rowKey') rowKey: string, + @Param('id') id: string, @Body() pilotDto: PilotDto ) { try { - let pilot = new Pilot(); - - pilot = { - partitionKey: pilotDto.partitionKey, - rowKey: pilotDto.rowKey, - id: pilotDto.id, - name: pilotDto.name, - address: pilotDto.address, - city: pilotDto.city, - state: pilotDto.state, - postalCode: pilotDto.postalCode, - email: pilotDto.email, - phone: pilotDto.phone, - medicalClass: pilotDto.medicalClass, - medicalExpiration: pilotDto.medicalExpiration, - certificates: JSON.stringify(pilotDto.certificates), - endorsements: JSON.stringify(pilotDto.endorsements) - } - - return await this.pilotService.update(partitionKey, rowKey, pilot); + return await this.pilotService.update(id, pilotDto); } catch (error) { const customError = error as CustomError; @@ -113,14 +77,13 @@ export class PilotController { } } + @Delete(':id') @UseGuards(AuthGuard) - @Delete(':partitionKey/:rowKey') async delete( - @Param('partitionKey') partitionKey: string, - @Param('rowKey') rowKey: string + @Param('id') id: string, ) { try { - return await this.pilotService.delete(partitionKey, rowKey); + return await this.pilotService.delete(id); } catch (error) { const customError = error as CustomError; diff --git a/api/src/pilot/pilot.dto.ts b/api/src/pilot/pilot.dto.ts index 7eddeeb..1b6c0ac 100644 --- a/api/src/pilot/pilot.dto.ts +++ b/api/src/pilot/pilot.dto.ts @@ -1,10 +1,6 @@ -import { Certificate } from "./certificate/certificate.entity"; -import { Endorsement } from "./endorsement/endorsement.entity"; +import { LogEntity } from "src/log/log.entity"; export class PilotDto { - partitionKey: string; - rowKey: string; - id: string; name: string; address: string; city: string; @@ -12,8 +8,4 @@ export class PilotDto { postalCode: string; email?: string; phone?: string; - medicalClass?: string; - medicalExpiration?: string; - certificates: Certificate; - endorsements: Endorsement } diff --git a/api/src/pilot/pilot.entity.ts b/api/src/pilot/pilot.entity.ts index 6b8b7d8..d78a017 100644 --- a/api/src/pilot/pilot.entity.ts +++ b/api/src/pilot/pilot.entity.ts @@ -1,18 +1,47 @@ -import { EntityString } from '@noahspan/azure-database'; +import { LogEntity } from 'src/log/log.entity'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import { CertificateEntity } from '../certificate/certificate.entity'; +import { EndorsementEntity } from 'src/endorsement/endorsement.entity'; +import { MedicalEntity } from 'src/medical/medical.entity'; -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; +@Entity({ name: 'pilots' }) +export class PilotEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column() + address: string + + @Column() + city: string; + + @Column() + state: string; + + @Column() + postalCode: string; + + @Column() + email: string; + + @Column() + phone: string; + + @Column() + userId: string | null; + + @OneToMany(() => LogEntity, (log: LogEntity) => log.pilot) + logs: LogEntity[]; + + @OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot) + certificates: CertificateEntity[]; + + @OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot) + endorsements: EndorsementEntity[]; + + @OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot) + medical: MedicalEntity[]; } diff --git a/api/src/pilot/pilot.interceptor.ts b/api/src/pilot/pilot.interceptor.ts new file mode 100644 index 0000000..34bf2db --- /dev/null +++ b/api/src/pilot/pilot.interceptor.ts @@ -0,0 +1,51 @@ +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; +import { jwtDecode } from 'jwt-decode'; +import { Observable, map } from 'rxjs'; +import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface'; +import { PilotEntity } from './pilot.entity'; +import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules'; +import { Reflector } from '@nestjs/core'; + +export class PilotInterceptor implements NestInterceptor { + constructor(private reflector: Reflector) {} + + intercept(context: ExecutionContext, handler: CallHandler): Observable { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + return handler.handle().pipe( + map((data: PilotEntity[]) => { + const req = context.switchToHttp().getRequest(); + const limitData = (data) => { + return data.map((pilot: PilotEntity) => { + return { + id: pilot.id, + name: pilot.name + }; + }) + } + console.log(req.headers.authorization) + if (req.headers.authorization) { + const authHeader = req.headers.authorization; + const token = authHeader && authHeader.split(' ')[1]; + const jwtPayload: CustomJwtPayload = jwtDecode(token); + + if (jwtPayload.roles.includes('Flying.Read')) { + const pilots = limitData(data) + + return pilots; + } else { + return data; + } + } else if (!req.headers.authorization && isPublic) { + const publicData = limitData(data); + const logs = publicData.slice(0,5) + + return logs; + } + }) + ); + } +} diff --git a/api/src/pilot/pilot.module.ts b/api/src/pilot/pilot.module.ts index 78608b3..7fea858 100644 --- a/api/src/pilot/pilot.module.ts +++ b/api/src/pilot/pilot.module.ts @@ -1,41 +1,15 @@ import { Module } from '@nestjs/common'; import { PilotController } from './pilot.controller'; import { PilotService } from './pilot.service'; -import { AzureTableStorageModule } from '@noahspan/azure-database'; -import { Pilot } from './pilot.entity'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { Log } from 'src/log/log.entity'; +import { PilotEntity } from './pilot.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ - AzureTableStorageModule.forRootAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => { - return { - connectionString: configService.get('azureStorageConnectionString') - }; - }, - inject: [ConfigService] - }), - AzureTableStorageModule.forFeature(Log, { - createTableIfNotExists: false, - table: 'logs' - }), - AzureTableStorageModule.forRootAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => { - return { - connectionString: configService.get('azureStorageConnectionString') - }; - }, - inject: [ConfigService] - }), - AzureTableStorageModule.forFeature(Pilot, { - createTableIfNotExists: false, - table: 'pilots' - }) + TypeOrmModule.forFeature([PilotEntity]) ], controllers: [PilotController], + exports: [PilotService], providers: [PilotService] }) export class PilotModule {} diff --git a/api/src/pilot/pilot.service.ts b/api/src/pilot/pilot.service.ts index 3cc321e..15a44b0 100644 --- a/api/src/pilot/pilot.service.ts +++ b/api/src/pilot/pilot.service.ts @@ -1,54 +1,46 @@ -import { InjectRepository, Repository } from '@noahspan/azure-database'; -import { Inject, Injectable } from '@nestjs/common'; -import { Pilot } from './pilot.entity'; -import { Log } from 'src/log/log.entity'; -import { LogService } from 'src/log/log.service'; +import { Injectable } from '@nestjs/common'; +import { PilotEntity } from './pilot.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm'; +import { PilotDto } from './pilot.dto'; +import { CustomError } from 'src/error/customError'; @Injectable() export class PilotService { constructor( - @InjectRepository(Pilot) private readonly pilotRepository: Repository, - @InjectRepository(Log) private readonly logRepository: Repository + @InjectRepository(PilotEntity) private readonly pilotRepository: Repository ) {} - async find(partitionKey: string, rowKey: string): Promise { - return await this.pilotRepository.find(partitionKey, rowKey); + async find(id: string): Promise { + try { + const pilotEntity = await this.pilotRepository.findOneBy({ id }); + + if (pilotEntity) { + return pilotEntity + } else { + throw new CustomError('Pilot not found', 'Not found', 404) + } + } catch (error) { + throw error + } } - async findAll(): Promise { - return await this.pilotRepository.findAll(); + async findAll(): Promise { + return await this.pilotRepository.find(); } - async create(pilot: Pilot): Promise { - // try { - // return await this.pilotRepository.create(pilot); - // } catch (error) { - // throw new Error(error); - // } - return await this.pilotRepository.create(pilot); + async create(pilot: PilotDto): Promise { + return await this.pilotRepository.insert(pilot); } async update( - partitionKey: string, - rowKey: string, - pilot: Pilot - ): Promise { - return await this.pilotRepository.update(partitionKey, rowKey, pilot); + id: string, + pilot: PilotDto + ): Promise { + return await this.pilotRepository.update(id, pilot); } - async delete(partitionKey: string, rowKey: string): Promise { - const pilotLogs: Log[] = await this.logRepository.findAll({ - queryOptions: { - filter: `pilotId eq '${rowKey}'` - } - }) - - for (const pilotLog of pilotLogs) { - await this.logRepository.delete(pilotLog.partitionKey, pilotLog.rowKey); - } - - await this.pilotRepository.delete(partitionKey, rowKey); - - return + async delete(id: string): Promise { + return await this.pilotRepository.delete({ id }); } } diff --git a/api/src/track/track.controller.ts b/api/src/track/track.controller.ts new file mode 100644 index 0000000..594e8a6 --- /dev/null +++ b/api/src/track/track.controller.ts @@ -0,0 +1,80 @@ +import { + Controller, + Delete, + Get, + HttpException, + Param, + Post, + Query, + UploadedFile, + UseGuards, + UseInterceptors +} from '@nestjs/common'; +import { AuthGuard } from '@noahspan/noahspan-modules'; +import { CustomError } from '../error/customError'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { FileService } from '../file/file.service'; +import { TrackService } from './track.service'; +import { TrackEntity } from './track.entity'; +import { DeleteResult, InsertResult } from 'typeorm'; + +@Controller('tracks') +export class TrackController { + constructor( + private readonly fileService: FileService, + private readonly trackService: TrackService + ) {} + + @UseGuards(AuthGuard) + @Get(':logId') + async findAll(@Param('logId') logId: string): Promise { + try { + return await this.trackService.findAll(logId); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @UseGuards(AuthGuard) + @Post(':logId/:order') + @UseInterceptors(FileInterceptor('file')) + async create(@Param('logId') logId: string, @Param('order') order: number, @UploadedFile() file: Express.Multer.File): Promise { + try { + return await this.trackService.create(logId, order, file); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @UseGuards(AuthGuard) + @Delete(':id/:filename/:logId') + async delete(@Param('id') id: string, @Query('fileName') filename: string, @Query('logId') logId: string): Promise { + try { + return await this.trackService.delete(id, logId, filename); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } + + @Get(':logId/:fileName') + async downloadTrack(@Param('logId') logId: string, @Param('fileName') fileName: string): Promise { + try { + return await this.trackService.downloadTrackFile(logId, fileName); + } catch (error) { + const customError = error as CustomError; + + throw new HttpException(customError.message, customError.statusCode); + } + } +} + + + + + diff --git a/api/src/track/track.dto.ts b/api/src/track/track.dto.ts new file mode 100644 index 0000000..217c80e --- /dev/null +++ b/api/src/track/track.dto.ts @@ -0,0 +1,5 @@ +export class TrackDto { + logId: string; + order: number; + url?: string; +} \ No newline at end of file diff --git a/api/src/track/track.entity.ts b/api/src/track/track.entity.ts new file mode 100644 index 0000000..af3e65a --- /dev/null +++ b/api/src/track/track.entity.ts @@ -0,0 +1,18 @@ +import { LogEntity } from 'src/log/log.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; + +@Entity({ name: 'tracks' }) +export class TrackEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + url: string; + + @Column() + order: number; + + @ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ name: 'logId' }) + log: LogEntity; +} \ No newline at end of file diff --git a/api/src/track/track.module.ts b/api/src/track/track.module.ts new file mode 100644 index 0000000..50460ca --- /dev/null +++ b/api/src/track/track.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { TrackEntity } from "./track.entity"; +import { TrackController } from "./track.controller"; +import { FileService } from "../file/file.service"; +import { TrackService } from './track.service'; +import { LogModule } from 'src/log/log.module'; + +@Module({ + imports: [ + LogModule, + TypeOrmModule.forFeature([TrackEntity]) + ], + controllers: [TrackController], + providers: [ + FileService, + TrackService + ] +}) +export class TrackModule {} \ No newline at end of file diff --git a/api/src/track/track.service.ts b/api/src/track/track.service.ts new file mode 100644 index 0000000..9414905 --- /dev/null +++ b/api/src/track/track.service.ts @@ -0,0 +1,98 @@ +import { DeleteResult, InsertResult, Repository, UpdateResult } from "typeorm"; +import { TrackDto } from "./track.dto"; +import { TrackEntity } from "./track.entity"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Injectable } from "@nestjs/common"; +import { LogService } from "src/log/log.service"; +import { LogEntity } from "src/log/log.entity"; +import { CustomError } from "src/error/customError"; +import { FileService } from "src/file/file.service"; + +@Injectable() +export class TrackService { + private readonly containerName: string = 'tracks' + + constructor( + @InjectRepository(TrackEntity) private readonly trackRepository: Repository, + private readonly fileService: FileService, + private readonly logService: LogService + ) {} + + async find(id: string): Promise { + try { + return await this.trackRepository.findOneBy({ id }); + } catch (error) { + throw new CustomError('Track not found', 'Not found', 404) + } + } + + async findAll(logId: string): Promise { + try { + const logEntity: LogEntity = await this.logService.find(logId); + + if (logEntity) { + const tracks = await this.trackRepository.find({ + where: { log: + { + id: logEntity.id + } + }, + }) + + return tracks; + } + } catch (error) { + console.log(error) + throw new CustomError('Tracks not found', 'Not found', 404); + } + } + + async create(logId: string, order: number, file: Express.Multer.File): Promise { + try { + const logEntity: LogEntity = await this.logService.find(logId); + + if (logEntity) { + const url = await this.fileService.uploadFile(file, this.containerName, logId); + const track = this.trackRepository.create({ + log: logEntity, + order: order, + url: url + }); + + return await this.trackRepository.insert(track); + } else { + throw new CustomError('Log not found', 'Not found', 404) + } + } catch (error) { + throw error; + } + } + + async update(id: string, track: TrackDto): Promise { + try { + return await this.trackRepository.update(id, track); + } catch(error) { + throw error + } + } + + async downloadTrackFile(logId: string, fileName: string): Promise { + try { + const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName); + + return downloadedFile; + } catch (error) { + throw error + } + } + + async delete(id: string, logId: string, fileName: string): Promise { + try { + await this.fileService.deleteFile(this.containerName, logId, fileName); + + return await this.trackRepository.delete({ id }); + } catch (error) { + throw error + } + } +} \ No newline at end of file diff --git a/app/src/auth/msalConfig.ts b/app/src/auth/msalConfig.ts deleted file mode 100644 index 24c7b80..0000000 --- a/app/src/auth/msalConfig.ts +++ /dev/null @@ -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" -// }; \ No newline at end of file diff --git a/app/src/components/actionMenu/ActionMenu.tsx b/app/src/components/actionMenu/ActionMenu.tsx deleted file mode 100644 index 59851d3..0000000 --- a/app/src/components/actionMenu/ActionMenu.tsx +++ /dev/null @@ -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 - ); - const isAuthenticated = useIsAuthenticated(); - - const onOpenActionMenu = (event: React.MouseEvent) => { - setAnchorElAction(event.currentTarget); - }; - - const onCloseActionMenu = () => { - setAnchorElAction(null); - }; - - return ( -
- - - - - {isAuthenticated && - <> - onOpenCloseForm(FormMode.EDIT, id)}> - - - - Edit - - {onOpenCloseTracks && - onOpenCloseTracks!(FormMode.EDIT, id)}> - - - - Tracks - - } - - - } - onOpenCloseForm(FormMode.VIEW, id)}> - - - - View - - {isAuthenticated && - <> -
- onDelete(id)}> - - - - Delete - - - } -
-
- ); -}; - -export default ActionMenu; diff --git a/app/src/components/confirmationDialog/ConfirmationDialog.tsx b/app/src/components/confirmationDialog/ConfirmationDialog.tsx deleted file mode 100644 index 90a2628..0000000 --- a/app/src/components/confirmationDialog/ConfirmationDialog.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Icon, - IconName, - Spinner -} from '@noahspan/noahspan-components'; -import { IDialogConfirmationProps } from './IConfirmationDialogProps'; - -const ConfirmationDialog = ({ - contentText, - isLoading, - isOpen, - onCancel, - onConfirm, - title -}: IDialogConfirmationProps) => { - return ( - - {title} - - {!isLoading && {contentText}} - {isLoading && } - - - - - - - ); -}; - -export default ConfirmationDialog; diff --git a/app/src/components/flights/Flights.tsx b/app/src/components/flights/Flights.tsx deleted file mode 100644 index 05ad37e..0000000 --- a/app/src/components/flights/Flights.tsx +++ /dev/null @@ -1,76 +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([]); - const { logs, isLoading } = useLogs(); - const isMedium = useMediaQuery(theme.breakpoints.up('md')); - - useEffect(() => { - const flights: ILogbookEntry[] | undefined = logs?.filter((log: ILogbookEntry) => { - if (log.tracks && JSON.parse(log.tracks).length > 0) { - return log; - } - }) - - if (flights && flights.length > 0) { - setFlights(flights) - } - }, [logs]) - - return ( - - - - - Flights - - {!isLoading && - - - - } - {isLoading && [...Array(6)].map((_element, index) => { - return ( - - - - - - - - - - - - - {[...Array(4)].map((_element, index) => { - return ( - <> - - - - ) - })} - - - - - - ) - })} - - - - ) -} - -export default Flights; \ No newline at end of file diff --git a/app/src/components/logForm/ILogFormProps.ts b/app/src/components/logForm/ILogFormProps.ts deleted file mode 100644 index 0cb9d9b..0000000 --- a/app/src/components/logForm/ILogFormProps.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FormMode } from '../../enums/formMode'; - -export interface ILogFormProps { - entryId?: string; - isDrawerOpen: boolean; - mode: FormMode; - onOpenClose: (mode: FormMode) => void; -} diff --git a/app/src/components/logForm/ILogFormState.ts b/app/src/components/logForm/ILogFormState.ts deleted file mode 100644 index e8a994b..0000000 --- a/app/src/components/logForm/ILogFormState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Alert } from "../../interfaces/Alert.interface"; - -export interface ILogFormState { - alert: Alert | undefined; - isDisabled: boolean; - isLoading: boolean; - pilotOptions: { label: string; value: string }[]; - selectedEntryPilotName: string; -} diff --git a/app/src/components/logForm/LogForm.tsx b/app/src/components/logForm/LogForm.tsx deleted file mode 100644 index e379045..0000000 --- a/app/src/components/logForm/LogForm.tsx +++ /dev/null @@ -1,947 +0,0 @@ -import React, { useEffect, useReducer } from 'react'; -import { - Accordion, - AccordionDetails, - AccordionSummary, - Alert, - Button, - DatePicker, - Drawer, - Grid, - Icon, - IconButton, - IconName, - Select, - TextField, - theme, - Typography, - useMediaQuery -} from '@noahspan/noahspan-components'; -import { useForm, Controller, FormProvider } from 'react-hook-form'; -import { ILogFormProps } from './ILogFormProps'; -import { initialState, reducer } from './reducer'; -import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; -import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; -import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; -import { useIsAuthenticated } from '@azure/msal-react'; -import { FormMode } from '../../enums/formMode'; -import { usePilots } from '../../hooks/pilots/UsePilots'; - -const LogForm: React.FC = ({ - entryId, - isDrawerOpen, - mode, - onOpenClose -}) => { - const [state, dispatch] = useReducer(reducer, initialState); - const httpClient: AxiosInstance = useHttpClient(); - const { getAccessToken } = useAccessToken(); - const isAuthenticated = useIsAuthenticated(); - const defaultValues = { - pilotId: '', - pilotName: '', - date: null, - aircraftMakeModel: '', - aircraftIdentity: '', - routeFrom: '', - routeTo: '', - durationOfFlight: null, - singleEngineLand: null, - simulatorAtd: null, - landingsDay: null, - landingsNight: null, - groundTrainingReceived: null, - flightTrainingReceived: null, - crossCountry: null, - night: null, - solo: null, - pilotInCommand: null, - instrumentActual: null, - instrumentSimulated: null, - instrumentApproaches: null, - instrumentHolds: null, - instrumentNavTrack: null, - notes: '' - }; - const methods = useForm(); - const { pilots } = usePilots(); - const isMedium = useMediaQuery(theme.breakpoints.up('md')); - - const onCancel = () => { - methods.reset(defaultValues); - dispatch({ type: 'SET_IS_DISABLED', payload: false }); - onOpenClose(FormMode.CANCEL); - }; - - const onSubmit = async (data: unknown) => { - try { - dispatch({ type: 'SET_IS_LOADING', payload: true }); - - const accessToken: string = await getAccessToken(); - - if (!entryId) { - await httpClient.post(`api/logs`, data, { - headers: { - Authorization: accessToken - } - }); - } else { - await httpClient.put(`api/logs/log/${entryId}`, data, { - headers: { - Authorization: accessToken - } - }); - } - - methods.reset(defaultValues); - dispatch({ type: 'SET_IS_DISABLED', payload: false }); - onOpenClose(FormMode.CANCEL); - } catch (error) { - const axiosError = error as AxiosError; - - dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }}); - } finally { - dispatch({ type: 'SET_IS_LOADING', payload: false }); - } - }; - - useEffect(() => { - if (mode === FormMode.VIEW) { - dispatch({ type: 'SET_IS_DISABLED', payload: true }); - } - }, [mode]); - - useEffect(() => { - const getEntry = async () => { - try { - dispatch({ type: 'SET_IS_LOADING', payload: true }); - - const config = isAuthenticated - ? { headers: { Authorization: await getAccessToken() } } - : {}; - const response: AxiosResponse = await httpClient.get( - `api/logs/log/${entryId}`, - config - ); - const entry = response.data; - - // if (mode !== FormMode.ADD) { - // const pilot = pilots?.find((pilot) => pilot.id === entry.pilotId); - // console.log(pilot.name) - // dispatch({ - // type: 'SET_SELECTED_ENTRY_PILOT_NAME', - // payload: pilot.name - // }); - // } - - methods.reset(entry); - } catch (error) { - const axiosError = error as AxiosError; - - dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }}); - } finally { - dispatch({ type: 'SET_IS_LOADING', payload: false }); - } - }; - - if (entryId && isDrawerOpen) { - getEntry(); - } - }, [entryId]); - - useEffect(() => { - if (pilots && FormMode.ADD) { - const newPilotsOptions = pilots.map((pilot) => { - return { - label: pilot.name, - value: pilot.id - }; - }); - - dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions }); - } - }, [pilots]); - - return ( - - -
- - - {`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Entry`} - - - - - - - {state.alert && ( - - - dispatch({ type: 'SET_ALERT', payload: undefined }) - } - severity={state.alert.severity} - sx={{ width: '100%' }} - > - {state.alert.message} - - - )} - - Pilot * - - - { - return ( - - - )} - - - } - {mode === FormMode.VIEW && - - } - - {state.isConfirmDialogOpen && ( - - )} - - - ) -} - -export default LogTracks; \ No newline at end of file diff --git a/app/src/components/logTracks/LogTracksState.interface.ts b/app/src/components/logTracks/LogTracksState.interface.ts deleted file mode 100644 index 6c2984c..0000000 --- a/app/src/components/logTracks/LogTracksState.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface LogTracksState { - isConfirmDialogOpen: boolean; - isConfirmDialogLoading: boolean; - isLoading: boolean; - selectedTrack: { - fileName: string, - index: number - } | undefined; - tracks: string[]; -} \ No newline at end of file diff --git a/app/src/components/logbook/Logbook.tsx b/app/src/components/logbook/Logbook.tsx deleted file mode 100644 index 9764945..0000000 --- a/app/src/components/logbook/Logbook.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { useEffect, useReducer } from 'react'; -import LogForm from '../logForm/LogForm'; -import { - Alert, - Box, - Button, - ColumnDef, - Grid, - Icon, - IconButton, - IconName, - Spinner, - Table, - theme, - Typography, - useMediaQuery -} from '@noahspan/noahspan-components'; -import { initialState, reducer } from './reducer'; -import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; -import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; -import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; -import { useIsAuthenticated } from '@azure/msal-react'; -import { FormMode } from '../../enums/formMode'; -import { authColumns, unauthColumns } from './columns'; -import ActionMenu from '../actionMenu/ActionMenu'; -import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog'; -import { ILogbookEntry } from './ILogbookEntry'; -import LogbookCard from '../logbookCard/LogbookCard'; -import LogTracks from '../logTracks/LogTracks'; - -const Logbook: React.FC = () => { - const [state, dispatch] = useReducer(reducer, initialState); - const httpClient: AxiosInstance = useHttpClient(); - const isAuthenticated = useIsAuthenticated(); - const { getAccessToken } = useAccessToken(); - const isMedium = useMediaQuery(theme.breakpoints.up('md')); - const actionsColumn: ColumnDef = { - header: 'Actions', - meta: { - align: 'center', - headerAlign: 'center' - }, - cell: (info: any) => ( - - ) - } - const tracksColumn: ColumnDef = { - accessorKey: 'tracks', - header: 'Tracks', - cell: (info: any) => { - if (info.row.original.tracks && JSON.parse(info.row.original.tracks).length > 0) { - return ( - onOpenCloseTracks(FormMode.VIEW, info.row.original.rowKey)}> - ) - } - } - } - - const getLogbookEntries = async () => { - try { - dispatch({ type: 'SET_IS_LOADING', payload: true }); - - const config = isAuthenticated - ? { headers: { Authorization: await getAccessToken() } } - : {}; - const response: AxiosResponse = await httpClient.get(`api/logs`, config); - const entries: ILogbookEntry[] = response.data; - - entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) - - if (response.data.length > 0) { - dispatch({ type: 'SET_ENTRIES', payload: response.data }); - - if (state.alert) { - dispatch({ type: 'SET_ALERT', payload: undefined}) - } - } else { - dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No logbook entries found.'}}) - } - } catch (error) { - const axiosError = error as AxiosError; - - dispatch({ - type: 'SET_ALERT', - payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`} - }); - } finally { - dispatch({ type: 'SET_IS_LOADING', payload: false }); - } - }; - - const onOpenCloseEntryForm = (mode: FormMode, entryId?: string) => { - switch (mode) { - case FormMode.ADD: - case FormMode.EDIT: - case FormMode.VIEW: - dispatch({ - type: 'SET_OPEN_CLOSE_ENTRY_FORM', - payload: { - formMode: mode, - selectedEntryId: entryId, - isFormOpen: true - } - }); - - break; - case FormMode.CANCEL: - dispatch({ - type: 'SET_OPEN_CLOSE_ENTRY_FORM', - payload: { - formMode: mode, - selectedEntryId: undefined, - isFormOpen: false - } - }); - - break; - } - }; - - const onOpenCloseTracks = (mode: FormMode, rowKey?: string) => { - switch(mode) { - case FormMode.EDIT: - case FormMode.VIEW: - dispatch({ - type: 'SET_OPEN_CLOSE_TRACKS', - payload: { - tracksMode: mode, - isTracksOpen: true, - selectedRowKey: rowKey - } - }) - - break; - case FormMode.CANCEL: - dispatch({ - type: 'SET_OPEN_CLOSE_TRACKS', - payload: { - tracksMode: mode, - isTracksOpen: false, - selectedRowKey: undefined - } - }) - - break; - } - } - - const onDeleteEntry = (entryId: string) => { - dispatch({ - type: 'SET_DELETE', - payload: { isConfirmationDialogOpen: true, selectedEntryId: entryId } - }); - }; - - const onConfirmationDialogConfirm = async () => { - try { - dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true }); - - const token = await getAccessToken(); - const config = isAuthenticated - ? { headers: { Authorization: `${token}` } } - : {}; - - await httpClient.delete(`api/logs/log/${state.selectedEntryId}`, config); - - dispatch({ - type: 'SET_DELETE', - payload: { isConfirmationDialogOpen: false, selectedEntryId: undefined } - }); - await getLogbookEntries(); - } catch (error) { - const axiosError = error as AxiosError; - - dispatch({ - type: 'SET_ALERT', - payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`} - }); - } finally { - dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false }); - } - }; - - const onConfirmationDialogCancel = () => { - dispatch({ - type: 'SET_DELETE', - payload: { isConfirmationDialogOpen: false, selectedEntryId: undefined } - }); - }; - - useEffect(() => { - let newColumns: ColumnDef[]; - - if (isAuthenticated) { - newColumns = [...authColumns]; - } else { - newColumns = [...unauthColumns]; - } - - const actionsColumnExists = newColumns.find((column) => column.id === 'actions'); - const tracksColumnExists = newColumns.find((column) => column.id === 'actions'); - - if (!actionsColumnExists) { - newColumns.push(actionsColumn); - } - - if (!tracksColumnExists) { - const notesColumnIndex = newColumns.findIndex((column) => column.id === 'notes') - - newColumns.splice(notesColumnIndex, 0, tracksColumn) - } - - dispatch({ type: 'SET_COLUMNS', payload: newColumns }) - }, [isAuthenticated]) - - useEffect(() => { - if (!state.isFormOpen) { - getLogbookEntries(); - } - }, [state.isFormOpen, state.isTracksOpen]); - - return ( - - - - Logbook - - - {isAuthenticated && - - } - - {!state.isLoading && state.alert && ( - - - dispatch({ type: 'SET_ALERT', payload: undefined }) - } - severity={state.alert.severity} - sx={{ width: '100%' }} - > - {state.alert.message} - - - )} - {!state.isLoading && ( - - {isMedium && state.columns && state.columns.length > 0 && state.entries.length > 0 && ( - - )} - {!isMedium && state.entries.length > 0 && - - } - - )} - {state.isLoading && !state.alert && ( - <> - - - - - Loading... - - - )} - - {state.isFormOpen && ( - onOpenCloseEntryForm(mode)} - /> - )} - {state.isConfirmDialogOpen && ( - - )} - {state.isTracksOpen && - onOpenCloseTracks(mode)} - selectedRowKey={state.selectedEntryId} - /> - } - - ); -}; - -export default Logbook; diff --git a/app/src/components/logbook/reducer.ts b/app/src/components/logbook/reducer.ts deleted file mode 100644 index ee58a4e..0000000 --- a/app/src/components/logbook/reducer.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ColumnDef } from '@noahspan/noahspan-components'; -import { FormMode } from '../../enums/formMode'; -import { Alert } from '../../interfaces/Alert.interface'; -import { ILogbookEntry } from './ILogbookEntry'; -import { ILogbookState } from './ILogbookState'; - -type Action = - | { type: 'SET_COLUMNS'; payload: ColumnDef[] } - | { - type: 'SET_DELETE'; - payload: { - isConfirmationDialogOpen: boolean; - selectedEntryId: string | undefined; - }; - } - | { type: 'SET_ENTRIES'; payload: ILogbookEntry[] } - | { type: 'SET_ALERT'; payload: Alert | undefined } - | { type: 'SET_FORM_MODE'; payload: FormMode } - | { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean } - | { type: 'SET_IS_LOADING'; payload: boolean } - | { - type: 'SET_OPEN_CLOSE_ENTRY_FORM'; - payload: { - formMode: FormMode; - selectedEntryId: string | undefined; - isFormOpen: boolean; - }; - } - | { type: 'SET_OPEN_CLOSE_TRACKS'; payload: { tracksMode: FormMode, selectedRowKey: string | undefined, isTracksOpen: boolean; }}; - -export const initialState: ILogbookState = { - alert: undefined, - columns: [], - entries: [], - formMode: FormMode.CANCEL, - isConfirmDialogLoading: false, - isConfirmDialogOpen: false, - isFormOpen: false, - isLoading: false, - isTracksOpen: false, - selectedEntryId: undefined, - tracksMode: FormMode.CANCEL -}; - -export const reducer = ( - state: ILogbookState, - action: Action -): ILogbookState => { - switch (action.type) { - case 'SET_COLUMNS': { - return { - ...state, - columns: action.payload - } - } - case 'SET_DELETE': { - return { - ...state, - isConfirmDialogOpen: action.payload.isConfirmationDialogOpen, - selectedEntryId: action.payload.selectedEntryId - }; - } - case 'SET_ENTRIES': { - return { - ...state, - entries: action.payload - }; - } - case 'SET_ALERT': { - return { - ...state, - alert: action.payload - }; - } - case 'SET_FORM_MODE': { - return { - ...state, - formMode: action.payload - }; - } - case 'SET_IS_CONFIRMATION_DIALOG_LOADING': { - return { - ...state, - isConfirmDialogLoading: action.payload - }; - } - case 'SET_IS_LOADING': { - return { - ...state, - isLoading: action.payload - }; - } - case 'SET_OPEN_CLOSE_ENTRY_FORM': { - return { - ...state, - formMode: action.payload.formMode, - isFormOpen: action.payload.isFormOpen, - selectedEntryId: action.payload.selectedEntryId - }; - } - case 'SET_OPEN_CLOSE_TRACKS': { - return { - ...state, - tracksMode: action.payload.tracksMode, - isTracksOpen: action.payload.isTracksOpen, - selectedEntryId: action.payload.selectedRowKey - } - } - default: { - return state; - } - } -}; diff --git a/app/src/components/logbookCard/LogbookCard.tsx b/app/src/components/logbookCard/LogbookCard.tsx deleted file mode 100644 index 3cbea20..0000000 --- a/app/src/components/logbookCard/LogbookCard.tsx +++ /dev/null @@ -1,101 +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 ( - - {logs.map((log) => { - console.log(log) - return ( - - - : null} - subheader={log.pilotName} - title={log.date} - slotProps={{ - subheader: { - fontSize: '16px' - }, - title: { - fontSize: '24px', - } - }} - /> - - - {mode === 'flights' && log.tracks && JSON.parse(log.tracks).length > 0 && - - - - } - - Aircraft Make and Model - - - {log.aircraftMakeModel} - - - Route From - - - {log.routeFrom} - - - Route To - - - {log.routeTo} - - - Duration Of Flight - - - {log.durationOfFlight} - - {mode === 'logbook' && log.tracks && JSON.parse(log.tracks).length > 0 && - <> - - Tracks - - {JSON.parse(log.tracks).map((track: string) => { - const trackSplit = track.split('/') - const filename = trackSplit[trackSplit.length - 1]; - - return ( - - {filename} - - ) - - })} - - } - {log.notes && - <> - - Notes - - - {log.notes} - - - } - - - - - - ) - })} - - ) -} - -export default LogbookCard; \ No newline at end of file diff --git a/app/src/components/pilotCard/PilotCard.tsx b/app/src/components/pilotCard/PilotCard.tsx deleted file mode 100644 index afa4b84..0000000 --- a/app/src/components/pilotCard/PilotCard.tsx +++ /dev/null @@ -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 ( - - {pilots.map((pilot) => { - return ( - - - } - title={pilot.name} - slotProps={{ - title: { - fontSize: '24px', - } - }} - /> - - - ) - })} - - ) -} - -export default PilotCard; \ No newline at end of file diff --git a/app/src/components/pilotForm/PilotForm.tsx b/app/src/components/pilotForm/PilotForm.tsx deleted file mode 100644 index 736b98f..0000000 --- a/app/src/components/pilotForm/PilotForm.tsx +++ /dev/null @@ -1,431 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useForm, Controller, FormProvider } from 'react-hook-form'; -import { - Button, - Drawer, - Grid, - Icon, - IconButton, - IconName, - PeoplePicker, - StateSelect, - TextField, - theme, - Typography, - useMediaQuery -} from '@noahspan/noahspan-components'; -import { IPilotFormProps } from './IPilotFormProps'; -import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; -import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; -import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; -import { useIsAuthenticated } from '@azure/msal-react'; -import { FormMode } from '../../enums/formMode'; -import { Person } from '@microsoft/microsoft-graph-types'; -import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates'; -import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements'; -import PilotFormMedical from '../pilotFormMedical/PilotFormMedical'; - -const PilotForm: React.FC = ({ - pilotId, - isDrawerOpen, - mode, - onOpenClose -}: IPilotFormProps) => { - const httpClient: AxiosInstance = useHttpClient(); - const [peoplePickerResults, setPeoplePickerResults] = useState([]); - const [isPeoplePickerLoading, setIsPeoplePickerLoading] = - useState(false); - const [selectedPerson, setSelectedPerson] = useState({ - userPrincipalName: '', - displayName: '' - }); - const [isLoading, setIsLoading] = useState(false); - const { getAccessToken } = useAccessToken(); - const isAuthenticated = useIsAuthenticated(); - const defaultValues = { - partitionKey: 'pilot', - rowKey: 'noah@noahspannbauer.com', - id: '', - name: '', - address: '', - city: '', - state: '', - postalCode: '', - email: '', - phone: '', - medicalClass: '', - medicalExpiration: '', - certificates: [], - endorsements: [] - }; - const methods = useForm({ - defaultValues: defaultValues - }); - const [isDisabled, setIsDisabled] = useState(false); - const [isError, setIsError] = useState(false); - const isMedium = useMediaQuery(theme.breakpoints.up('md')); - - const onPeoplePickerSearch = async ( - _event: React.SyntheticEvent, - value: string - ) => { - setIsPeoplePickerLoading(true); - - try { - if (value !== '') { - const searchString: string = value; - const accessToken: string = await getAccessToken(); - const response: AxiosResponse = await httpClient.get( - `api/user/search?search=${searchString}`, - { - headers: { - Authorization: accessToken - } - } - ); - - setPeoplePickerResults(response.data); - } else { - setPeoplePickerResults([]); - } - } catch (error) { - console.log(error); - } finally { - setIsPeoplePickerLoading(false); - } - }; - - const onPeoplePickerSelectionChange = ( - _event: React.SyntheticEvent, - value: Person, - _reason: string - ) => { - methods.setValue('id', value.userPrincipalName!.toString()); - methods.setValue('name', value.displayName!.toString()); - setSelectedPerson(value); - }; - - const onCancel = () => { - methods.reset(defaultValues); - onOpenClose(FormMode.CANCEL); - setSelectedPerson({ userPrincipalName: '', displayName: '' }); - setIsDisabled(false) - }; - - const onSubmit = async (data: unknown) => { - try { - setIsLoading(true); - - const accessToken: string = await getAccessToken(); - - if (!pilotId) { - await httpClient.post(`api/pilots`, data, { - headers: { - Authorization: accessToken - } - }); - } else { - await httpClient.put(`api/pilots/pilot/${pilotId}`, data, { - headers: { - Authorization: accessToken - } - }) - } - - methods.reset(defaultValues) - onOpenClose(FormMode.CANCEL); - } catch (error) { - const axiosError = error as AxiosError; - const responseData = axiosError.response?.data as any; - - console.log(responseData.message); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - if (mode === FormMode.VIEW) { - setIsDisabled(true); - } - }, [mode]); - - useEffect(() => { - const getPilot = async () => { - try { - setIsLoading(true); - - const config = isAuthenticated - ? { headers: { Authorization: await getAccessToken() } } - : {}; - const response: AxiosResponse = await httpClient.get( - `api/pilots/pilot/${pilotId}`, - config - ); - const pilot = response.data; - - pilot.certificates = JSON.parse(pilot.certificates); - pilot.endorsements = JSON.parse(pilot.endorsements) - - setSelectedPerson({ - userPrincipalName: pilot.id, - displayName: pilot.name - }); - methods.reset(pilot); - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - - if (pilotId) { - getPilot(); - } - }, [pilotId]); - - return ( - - - - - - {`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`} - - - - - - - - Name * - - - - - {isAuthenticated && - <> - - Address * - - - ( - - )} - /> - - - City * - - - ( - - )} - /> - - - State * - - - ( - - )} - /> - - - Postal Code * - - - ( - - )} - /> - - - Email - - - ( - - )} - /> - - - Phone Number - - - ( - - )} - /> - - - } - {isAuthenticated && - - - - } - - - - - - - - - {mode.toString() !== FormMode.VIEW && ( - - )} - - - - - - ); -}; - -export default PilotForm; diff --git a/app/src/components/pilots/Pilot.interface.ts b/app/src/components/pilots/Pilot.interface.ts deleted file mode 100644 index 965ed48..0000000 --- a/app/src/components/pilots/Pilot.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Pilot { - partitionKey: string; - rowKey: string; - id: string; - name: string; -}; \ No newline at end of file diff --git a/app/src/components/pilots/Pilots.tsx b/app/src/components/pilots/Pilots.tsx deleted file mode 100644 index d56b277..0000000 --- a/app/src/components/pilots/Pilots.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useEffect, useReducer, useState } from 'react'; -import PilotForm from '../pilotForm/PilotForm'; -import { - Alert, - Box, - Button, - ColumnDef, - Grid, - Icon, - IconName, - Table, - theme, - Typography, - useMediaQuery -} from '@noahspan/noahspan-components'; -import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; -import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; -import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; -import { useIsAuthenticated } from '@azure/msal-react'; -import { FormMode } from '../../enums/formMode'; -import { Pilot } from './Pilot.interface'; -import { initialState, reducer } from './reducer'; -import ActionMenu from '../actionMenu/ActionMenu'; -import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog'; -import PilotCard from '../pilotCard/PilotCard'; - -const Pilots: React.FC = () => { - const [state, dispatch] = useReducer(reducer, initialState); - const httpClient: AxiosInstance = useHttpClient(); - const isAuthenticated = useIsAuthenticated(); - const { getAccessToken } = useAccessToken(); - const isMedium = useMediaQuery(theme.breakpoints.up('md')); - - const getPilots = async () => { - try { - const response: AxiosResponse = await httpClient.get( - `api/pilots` - ); - - if (response.data.length > 0) { - dispatch({ type: 'SET_PILOTS', payload: response.data }); - - if (state.alert) { - dispatch({ type: 'SET_ALERT', payload: undefined }) - } - } else { - dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No pilots found.' }}) - } - } catch (error) { - const axiosError = error as AxiosError; - - dispatch({ - type: 'SET_ALERT', - payload: { severity: 'error', message: `Loading of pilots failed with the following message: ${axiosError.message}`} - }) - } finally { - dispatch({ type: 'SET_IS_LOADING', payload: false }) - } - }; - - const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => { - switch (mode) { - case FormMode.ADD: - case FormMode.EDIT: - case FormMode.VIEW: - dispatch({ - type: 'SET_OPEN_CLOSE_ENTRY_FORM', - payload: { - formMode: mode, - selectedPilotId: pilotId, - isFormOpen: true - } - }) - - break; - case FormMode.CANCEL: - dispatch({ - type: 'SET_OPEN_CLOSE_ENTRY_FORM', - payload: { - formMode: mode, - selectedPilotId: undefined, - isFormOpen: false - } - }) - - break; - } - }; - - const onDeleteEntry = (pilotId: string) => { - dispatch({ - type: 'SET_DELETE', - payload: { isConfirmDialogOpen: true, selectedPilotId: pilotId } - }); - }; - - const onConfirmationDialogConfirm = async () => { - try { - dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true }); - - const token = await getAccessToken(); - const config = isAuthenticated - ? { headers: { Authorization: `${token}` } } - : {}; - - await httpClient.delete(`api/pilots/pilot/${state.selectedPilotId}`, config); - - dispatch({ - type: 'SET_DELETE', - payload: { isConfirmDialogOpen: false, selectedPilotId: undefined } - }); - await getPilots(); - } catch (error) { - const axiosError = error as AxiosError; - - dispatch({ - type: 'SET_ALERT', - payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`} - }); - } finally { - dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false }); - } - }; - - const onConfirmationDialogCancel = () => { - dispatch({ - type: 'SET_DELETE', - payload: { isConfirmDialogOpen: false, selectedPilotId: undefined } - }); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: 'name', - header: 'Name' - }, - { - header: 'Actions', - cell: (info: any) => ( - - ) - } - ]; - - useEffect(() => { - if (!state.isFormOpen) { - getPilots(); - } - }, [state.isFormOpen]); - - return ( - - - - Pilots - - - {isAuthenticated && - - } - - {!state.isLoading && state.alert && ( - - - dispatch({ type: 'SET_ALERT', payload: undefined }) - } - severity={state.alert.severity} - sx={{ width: '100%' }} - > - {state.alert.message} - - - )} - - {isMedium && state.pilots.length > 0 && -
- } - {!isMedium && state.pilots.length > 0 && - - } - - - {state.isFormOpen && ( - onOpenClosePilotForm(mode)} - pilotId={state.selectedPilotId} - /> - )} - {state.isConfirmDialogOpen && ( - - )} - - ); -}; - -export default Pilots; diff --git a/app/src/components/siteNav/SiteNav.tsx b/app/src/components/siteNav/SiteNav.tsx deleted file mode 100644 index a447015..0000000 --- a/app/src/components/siteNav/SiteNav.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAppContext } from '../../hooks/appContext/UseAppContext'; -import { - Avatar, - Button, - Icon, - IconButton, - IconName, - Menu, - MenuItem, - Navbar, - Spinner, - Typography -} from '@noahspan/noahspan-components'; -import { useIsAuthenticated, useMsal } from '@azure/msal-react'; -import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; -import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; -import { AxiosInstance, AxiosResponse } from 'axios'; -import { User } from '@microsoft/microsoft-graph-types'; - -const SiteNav = () => { - const [loading, setLoading] = useState(false); - const [userPhoto, setUserPhoto] = useState(); - const [pages, setPages] = useState<{ name: string; url: string; }[]>([]); - const httpClient: AxiosInstance = useHttpClient(); - const appContext = useAppContext(); - const isAuthenticated = useIsAuthenticated(); - const { getAccessToken } = useAccessToken(); - const { inProgress, instance } = useMsal(); - const navigate = useNavigate(); - const getPages = () => { - const pages = [ - { - name: 'Flights', - url: '/' - }, - { - name: 'Logbook', - url: '/logbook' - }, - { - name: 'Pilots', - url: '/pilots' - } - ]; - - setPages(pages) - }; - const handleSignIn = () => { - instance.loginRedirect({ - scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`] - }); - }; - const handleSignOut = () => { - instance.logoutRedirect(); - }; - const getUserProfile = async (accessToken: string): Promise => { - try { - const response: AxiosResponse = await httpClient.get(`api/user/profile`, { - headers: { - Authorization: accessToken - } - }); - const userProfile: User = response.data; - - return userProfile; - } catch (error) { - throw new Error(); - } - }; - const getUserPhoto = async (accessToken: string): Promise => { - try { - const response: AxiosResponse = await httpClient.get(`api/user/photo`, { - headers: { - Authorization: accessToken - }, - responseType: 'arraybuffer' - }); - const arrayBufferView = new Uint8Array(response.data); - const blob = new Blob([arrayBufferView], { type: 'image/png' }); - const imageUrl = window.URL.createObjectURL(blob); - - return imageUrl; - } catch (error) { - throw new Error(); - } - }; - const handlePageClick = (url: string) => { - navigate(url); - }; - - const Settings = () => { - return ( - - - - Sign Out - - - ); - }; - - useEffect(() => { - const setUserProfile = async () => { - try { - setLoading(true); - - const accessToken: string = await getAccessToken(); - const userProfile = await getUserProfile(accessToken); - const userPhoto = await getUserPhoto(accessToken); - - setUserPhoto(userPhoto); - - appContext.dispatch({ - type: 'SET_USER_PROFILE', - payload: userProfile - }); - } catch (error) { - console.log(error); - } finally { - setLoading(false); - } - }; - - if ( - isAuthenticated && - Object.keys(appContext.state.userProfile).length === 0 - ) { - setUserProfile(); - getPages(); - } - }, [isAuthenticated]); - - useEffect(() => { - getPages(); - }, []) - - return ( - } - pages={pages} - settings={} - userPhoto={userPhoto} - /> - ); -}; - -export default SiteNav; \ No newline at end of file diff --git a/app/src/hooks/accessToken/UseAcessToken.tsx b/app/src/hooks/accessToken/UseAcessToken.tsx deleted file mode 100644 index 336d9e5..0000000 --- a/app/src/hooks/accessToken/UseAcessToken.tsx +++ /dev/null @@ -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 - }; -}; diff --git a/app/src/hooks/featureFlag/UseFeatureFlag.tsx b/app/src/hooks/featureFlag/UseFeatureFlag.tsx deleted file mode 100644 index 002f929..0000000 --- a/app/src/hooks/featureFlag/UseFeatureFlag.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useAppContext } from '../appContext/UseAppContext'; - -export const useFeatureFlag = (featureFlagKey: string) => { - const appContext = useAppContext(); - const featureFlag = appContext.state.featureFlags.find( - (featureFlag) => featureFlag.key === featureFlagKey - ); - console.log(featureFlag) - return featureFlag; -}; diff --git a/app/src/hooks/httpClient/UseHttpClient.tsx b/app/src/hooks/httpClient/UseHttpClient.tsx deleted file mode 100644 index 65937c1..0000000 --- a/app/src/hooks/httpClient/UseHttpClient.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import axios, { AxiosInstance, CreateAxiosDefaults } from 'axios'; - -export const useHttpClient = () => { - let config: CreateAxiosDefaults = { - baseURL: import.meta.env.VITE_API_URL, - headers: { - 'Content-Type': 'application/json' - } - }; - - // if (import.meta.env.DEV) { - // config = { - // baseURL: import.meta.env.VITE_API_URL, - // headers: { - // 'Content-Type': 'application/json' - // } - // }; - // } else { - // config = { - // headers: { - // 'Content-Type': 'application/json' - // } - // }; - // } - - const httpClient: AxiosInstance = axios.create(config); - - return httpClient; -}; diff --git a/app/src/hooks/logs/UseLogs.tsx b/app/src/hooks/logs/UseLogs.tsx deleted file mode 100644 index 80515d1..0000000 --- a/app/src/hooks/logs/UseLogs.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useState } from "react"; -import { useHttpClient } from "../httpClient/UseHttpClient"; -import { useAccessToken } from "../accessToken/UseAcessToken"; -import { useIsAuthenticated } from "@azure/msal-react"; -import { AxiosInstance, AxiosResponse } from "axios"; -import { ILogbookEntry } from "../../components/logbook/ILogbookEntry"; - -export const useLogs = () => { - const [logs, setLogs] = useState(); - const [isLoading, setIsLoading] = useState(false); - const httpClient: AxiosInstance = useHttpClient(); - const { getAccessToken } = useAccessToken(); - const isAuthenticated = useIsAuthenticated(); - - useEffect(() => { - const getLogs = async () => { - try { - setIsLoading(true); - - const config = isAuthenticated - ? { headers: { Authorization: await getAccessToken() } } - : {}; - const response: AxiosResponse = await httpClient.get( - `api/logs`, - config - ); - const logs: ILogbookEntry[] = response.data; - - logs.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) - - setLogs(logs) - } catch (error) { - return error; - } finally { - setIsLoading(false); - } - } - - getLogs(); - }, []) - - return { - logs, - isLoading - } -} \ No newline at end of file diff --git a/app/src/index.css b/app/src/index.css deleted file mode 100644 index 26e49ae..0000000 --- a/app/src/index.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - background-color: #f2f2f2; - margin: 8px; -} diff --git a/app/src/interfaces/Alert.interface.ts b/app/src/interfaces/Alert.interface.ts deleted file mode 100644 index 5bbdf16..0000000 --- a/app/src/interfaces/Alert.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Alert { - severity: 'success' | 'error' | 'info' | 'warning'; - message: string; -} \ No newline at end of file diff --git a/app/src/main.tsx b/app/src/main.tsx deleted file mode 100644 index ec3a6b8..0000000 --- a/app/src/main.tsx +++ /dev/null @@ -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( - - - - - - - - - - ); -}) \ No newline at end of file diff --git a/app/vite.config.ts b/app/vite.config.ts deleted file mode 100644 index dc1eec9..0000000 --- a/app/vite.config.ts +++ /dev/null @@ -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' - } -}); diff --git a/app/.eslintrc.cjs b/client/.eslintrc.cjs similarity index 100% rename from app/.eslintrc.cjs rename to client/.eslintrc.cjs diff --git a/app/.gitignore b/client/.gitignore similarity index 100% rename from app/.gitignore rename to client/.gitignore diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/index.html b/client/index.html similarity index 100% rename from app/index.html rename to client/index.html diff --git a/app/package.json b/client/package.json similarity index 54% rename from app/package.json rename to client/package.json index 78f2368..a2bc4f5 100644 --- a/app/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { - "name": "app", + "name": "client", "private": true, - "version": "1.4.0", + "version": "2.0.0-alpha", "type": "module", "scripts": { "dev": "vite", @@ -11,25 +11,32 @@ "serve": "serve -s dist" }, "dependencies": { - "@azure/msal-browser": "^4.0.1", - "@azure/msal-react": "^3.0.1", - "@noahspan/noahspan-components": "^1.9.1", + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.0", + "@noahspan/noahspan-components": "^2.0.0-alpha-14", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-table": "^8.21.3", "axios": "^1.7.2", + "daisyui": "^5.1.10", "dotenv": "^16.4.7", "leaflet": "^1.9.4", - "react": "19.0.0-rc.1", - "react-dom": "19.0.0-rc.1", + "oidc-spa": "^7.2.4", + "react": "^19.1.1", + "react-dom": "^19.1.1", "react-hook-form": "^7.51.4", - "react-leaflet": "^5.0.0", + "react-leaflet": "^4", "react-leaflet-kml": "^2.1.2", + "react-oidc-context": "^3.3.0", "react-router-dom": "^6.23.0", - "swiper": "^11.2.6" + "swiper": "^11.2.6", + "tailwindcss": "^4.1.13" }, "devDependencies": { "@microsoft/microsoft-graph-types": "^2.40.0", "@types/leaflet": "^1.9.16", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.8", "@vitejs/plugin-react": "^4.2.1", "typescript": "^5.2.2", "vite": "^5.2.0" diff --git a/app/public/layers-2x.png b/client/public/layers-2x.png similarity index 100% rename from app/public/layers-2x.png rename to client/public/layers-2x.png diff --git a/app/public/layers.png b/client/public/layers.png similarity index 100% rename from app/public/layers.png rename to client/public/layers.png diff --git a/app/public/marker-icon-2x.png b/client/public/marker-icon-2x.png similarity index 100% rename from app/public/marker-icon-2x.png rename to client/public/marker-icon-2x.png diff --git a/app/public/marker-icon.png b/client/public/marker-icon.png similarity index 100% rename from app/public/marker-icon.png rename to client/public/marker-icon.png diff --git a/app/public/marker-shadow.png b/client/public/marker-shadow.png similarity index 100% rename from app/public/marker-shadow.png rename to client/public/marker-shadow.png diff --git a/app/public/noahspan-logo.png b/client/public/noahspan-logo.png similarity index 100% rename from app/public/noahspan-logo.png rename to client/public/noahspan-logo.png diff --git a/app/public/vite.svg b/client/public/vite.svg similarity index 100% rename from app/public/vite.svg rename to client/public/vite.svg diff --git a/app/src/App.tsx b/client/src/App.tsx similarity index 62% rename from app/src/App.tsx rename to client/src/App.tsx index bb2fac4..9cbb496 100644 --- a/app/src/App.tsx +++ b/client/src/App.tsx @@ -3,29 +3,22 @@ import Flights from './components/flights/Flights'; import Logbook from './components/logbook/Logbook'; import Pilots from './components/pilots/Pilots'; import SiteNav from './components/siteNav/SiteNav'; -import { useIsAuthenticated } from '@azure/msal-react'; - -interface ProtectedRouteProps { - children: React.ReactNode; -} +import { HeroUIProvider } from '@heroui/react'; +import { useHref, useNavigate } from 'react-router-dom'; +import './styles.css'; const App = () => { - const isAuthenticated = useIsAuthenticated() - - const ProtectedRoute = ({ children }: ProtectedRouteProps) => { - return isAuthenticated ? children : - } + const navigate = useNavigate(); return ( - <> + } /> } /> } /> - - + ); }; diff --git a/app/src/assets/react.svg b/client/src/assets/react.svg similarity index 100% rename from app/src/assets/react.svg rename to client/src/assets/react.svg diff --git a/client/src/auth/oidcConfig.ts b/client/src/auth/oidcConfig.ts new file mode 100644 index 0000000..8e08f73 --- /dev/null +++ b/client/src/auth/oidcConfig.ts @@ -0,0 +1,11 @@ +import { createReactOidc } from "oidc-spa/react"; + +export const { OidcProvider, useOidc, getOidc } = 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`], + autoLogin: false, + postLoginRedirectUrl: '/', + noIframe: true +})); \ No newline at end of file diff --git a/client/src/components/actionMenu/ActionMenu.tsx b/client/src/components/actionMenu/ActionMenu.tsx new file mode 100644 index 0000000..4988493 --- /dev/null +++ b/client/src/components/actionMenu/ActionMenu.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { IActionMenuProps } from './IActionMenuProps'; +import { + IconButton, + Icon, + IconName, + Dropdown +} 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 + // ); + // const auth = useAuth(); + + // const onOpenActionMenu = (event: React.MouseEvent) => { + // setAnchorElAction(event.currentTarget); + // }; + + // const onCloseActionMenu = () => { + // setAnchorElAction(null); + // }; + + const options = [ + 'Item 1', + 'Item 2' + ] + + return ( + <> + console.log('clicked!')} + options={options} + > + + + + + + //
+ // + // + // + // + // {auth.isAuthenticated && + // <> + // onOpenCloseForm(FormMode.EDIT, id)}> + // + // + // + // Edit + // + // {onOpenCloseTracks && + // onOpenCloseTracks!(FormMode.EDIT, id)}> + // + // + // + // Tracks + // + // } + // + + // } + // onOpenCloseForm(FormMode.VIEW, id)}> + // + // + // + // View + // + // {auth.isAuthenticated && + // <> + //
+ // onDelete(id)}> + // + // + // + // Delete + // + // + // } + //
+ //
+ ); +}; + +export default ActionMenu; diff --git a/app/src/components/actionMenu/IActionMenuProps.tsx b/client/src/components/actionMenu/IActionMenuProps.tsx similarity index 100% rename from app/src/components/actionMenu/IActionMenuProps.tsx rename to client/src/components/actionMenu/IActionMenuProps.tsx diff --git a/client/src/components/confirmationDialog/ConfirmationDialog.tsx b/client/src/components/confirmationDialog/ConfirmationDialog.tsx new file mode 100644 index 0000000..2a467de --- /dev/null +++ b/client/src/components/confirmationDialog/ConfirmationDialog.tsx @@ -0,0 +1,52 @@ +// import { +// Button, +// Dialog, +// DialogActions, +// DialogContent, +// Icon, +// IconName, +// Loading +// } from '@noahspan/noahspan-components'; +import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalFooter, Spinner } from '@heroui/react' +import { DialogConfirmationProps } from './ConfirmationDialogProps.interface'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; + +const ConfirmationDialog = ({ + contentText, + isLoading, + isOpen, + onCancel, + onConfirm, + title +}: DialogConfirmationProps) => { + return ( + + + {title} + + {!isLoading &&
{contentText}
} + {isLoading && } +
+ + + + +
+
+ ); +}; + +export default ConfirmationDialog; diff --git a/app/src/components/confirmationDialog/IConfirmationDialogProps.ts b/client/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts similarity index 75% rename from app/src/components/confirmationDialog/IConfirmationDialogProps.ts rename to client/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts index 90cd809..1d7fd0e 100644 --- a/app/src/components/confirmationDialog/IConfirmationDialogProps.ts +++ b/client/src/components/confirmationDialog/ConfirmationDialogProps.interface.ts @@ -1,4 +1,4 @@ -export interface IDialogConfirmationProps { +export interface DialogConfirmationProps { contentText: string; isLoading: boolean; isOpen: boolean; diff --git a/client/src/components/flights/Flights.tsx b/client/src/components/flights/Flights.tsx new file mode 100644 index 0000000..691b0f1 --- /dev/null +++ b/client/src/components/flights/Flights.tsx @@ -0,0 +1,81 @@ +import { Icon, IconName, Skeleton } from "@noahspan/noahspan-components"; +import { Card } from '@heroui/react'; +import LogbookCard from "../logbookCard/LogbookCard"; +import { useEffect, useReducer } from "react"; +import { useLogs } from "../../hooks/logs/UseLogs"; +import { LogbookEntry } from "../logbook/LogbookEntry.interface"; +import { initialState, reducer } from "./reducer"; +import { Alert } from '@heroui/react' + +const Flights = () => { + const [state, dispatch] = useReducer(reducer, initialState); + const { logs, logsLoading } = useLogs(); + + useEffect(() => { + const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => { + if (log.tracks && log.tracks.length > 0) { + return log; + } + }) + + if (flights && flights.length > 0) { + dispatch({ type: 'SET_FLIGHTS', payload: flights}) + dispatch({ type: 'SET_ALERT', payload: undefined }) + } else { + dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No flights found' }}) + } + }, [logs]) + + useEffect(() => { + console.log(logsLoading) + }, [logsLoading]) + + return ( +
+
+

Flights

+
+ {!logsLoading && state.alert && ( +
+ + dispatch({ type: 'SET_ALERT', payload: undefined }) + } + color={state.alert.severity} + title={state.alert.message} + /> +
+ )} + {!logsLoading && +
+ +
+ } + {logsLoading && [...Array(6)].map((_element, index) => { + return ( +
+ +
+
+
+
+ {[...Array(4)].map((_element, index) => { + return ( + <> +
+
+ + ) + })} +
+
+
+ ) + })} +
+ ) +} + +export default Flights; \ No newline at end of file diff --git a/client/src/components/flights/FlightsState.interface.ts b/client/src/components/flights/FlightsState.interface.ts new file mode 100644 index 0000000..9bde5bf --- /dev/null +++ b/client/src/components/flights/FlightsState.interface.ts @@ -0,0 +1,8 @@ +import { Alert } from "../../interfaces/Alert.interface"; +import { LogbookEntry } from "../logbook/LogbookEntry.interface"; + +export interface FlightsState { + alert: Alert | undefined; + flights: LogbookEntry[]; + isLoading: boolean; +} \ No newline at end of file diff --git a/client/src/components/flights/reducer.tsx b/client/src/components/flights/reducer.tsx new file mode 100644 index 0000000..ae1ef29 --- /dev/null +++ b/client/src/components/flights/reducer.tsx @@ -0,0 +1,44 @@ +import { Alert } from "../../interfaces/Alert.interface"; +import { LogbookEntry } from "../logbook/LogbookEntry.interface"; +import { FlightsState } from "./FlightsState.interface"; + + +type Action = + | { type: 'SET_ALERT'; payload: Alert | undefined } + | { type: 'SET_FLIGHTS'; payload: LogbookEntry[] } + | { type: 'SET_IS_LOADING'; payload: boolean } + +export const initialState: FlightsState = { + alert: undefined, + flights: [], + isLoading: true +} + +export const reducer = ( + state: FlightsState, + action: Action +): FlightsState => { + switch (action.type) { + case 'SET_ALERT': { + return { + ...state, + alert: action.payload + } + } + case 'SET_FLIGHTS': { + return { + ...state, + flights: action.payload + } + } + case 'SET_IS_LOADING': { + return { + ...state, + isLoading: action.payload + } + } + default: { + return state + } + } +} \ No newline at end of file diff --git a/client/src/components/logForm/LogForm.tsx b/client/src/components/logForm/LogForm.tsx new file mode 100644 index 0000000..b9612ca --- /dev/null +++ b/client/src/components/logForm/LogForm.tsx @@ -0,0 +1,740 @@ +import React, { useEffect, useReducer } from 'react'; +import { Alert, Button, DatePicker, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Input, NumberInput, Select, Selection, SelectItem, SharedSelection, Textarea } from '@heroui/react' +import { useForm, Controller, FormProvider, useFormContext } from 'react-hook-form'; +import { LogFormProps } from './LogFormProps.interface'; +import { initialState, reducer } from './reducer'; +import { AxiosError, AxiosResponse } from 'axios'; +import { FormMode } from '../../enums/formMode'; +import { usePilots } from '../../hooks/pilots/UsePilots'; +import { useOidc } from '../../auth/oidcConfig'; +import httpClient from '../../httpClient/httpClient'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons' +import { parseAbsolute, parseDate, getLocalTimeZone, CalendarDate, ZonedDateTime } from '@internationalized/date'; +import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext'; + +const LogForm = () => { + const [state, dispatch] = useReducer(reducer, initialState); + const { control, formState, reset, setValue } = useFormContext() + const { pilots } = usePilots(); + const { isUserLoggedIn } = useOidc(); + const logbookContext = useLogbookContext() + + useEffect(() => { + if (logbookContext.state.formMode === FormMode.VIEW) { + dispatch({ type: 'SET_IS_DISABLED', payload: true }); + } + }, [logbookContext.state.formMode]); + + useEffect(() => { + const getEntry = async () => { + try { + dispatch({ type: 'SET_IS_LOADING', payload: true }); + + const response: AxiosResponse = await httpClient.get( + `api/logs/${logbookContext.state.selectedLogId}` + ); + const log = response.data; + + reset(log); + } catch (error) { + const axiosError = error as AxiosError; + + dispatch({ type: 'SET_ALERT', payload: { severity: 'danger', message: axiosError.message }}); + } finally { + dispatch({ type: 'SET_IS_LOADING', payload: false }); + } + }; + + if (logbookContext.state.selectedLogId) { + getEntry(); + } + }, [logbookContext.state.selectedLogId]); + + useEffect(() => { + if (pilots && FormMode.ADD) { + const newPilotsOptions = pilots.map((pilot) => { + return { + key: pilot.id, + label: pilot.name, + }; + }); + console.log(newPilotsOptions) + dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions }); + } + }, [pilots]); + + return ( +
+
+ Pilot +
+
+ { + return ( + + ); + }} + /> +
+
+ Date +
+
+ { + const parsedAbsoluteDate = value ? parseAbsolute(value, getLocalTimeZone()) : value + + return( + { + let date = selectedDate as CalendarDate; + + setValue('date', date.toDate(getLocalTimeZone()).toISOString()) + }} + size='lg' + value={parsedAbsoluteDate ? new CalendarDate(parsedAbsoluteDate.year, parsedAbsoluteDate.month, parsedAbsoluteDate.day) : value} + /> + ) + }} + /> +
+
+ Aircraft Make and Model +
+
+ ( + + )} + /> +
+ {isUserLoggedIn && + <> +
+ Aircraft Identity +
+
+ ( + + )} + /> +
+ + } +
+ Route From +
+
+ ( + + )} + /> +
+
+ Route To +
+
+ ( + + )} + /> +
+
+ Duration Of Flight +
+
+ ( + + )} + /> +
+ {isUserLoggedIn && + <> +
+ Single Engine Land +
+
+ ( + + )} + /> +
+
+ Simulator or ATD +
+
+ ( + + )} + /> +
+
+

Landings

+
+
+ Day +
+
+ ( + + )} + /> +
+
+ Night +
+
+ ( + + )} + /> +
+
+

Type of Training or Experience

+
+
+ + Ground Training Received + +
+
+ ( + + )} + /> +
+
+ + Flight Training Received + +
+
+ ( + + )} + /> +
+
+ Cross Country +
+
+ ( + + )} + /> +
+
+ Night +
+
+ ( + + )} + /> +
+
+ Solo +
+
+ ( + + )} + /> +
+
+ Pilot in Command +
+
+ ( + + )} + /> +
+
+

Instrument

+
+
+ Actual +
+
+ ( + + )} + /> +
+
+ Simulated +
+
+ ( + + )} + /> +
+
+ + Instrument Approaches + +
+
+ ( + + )} + /> +
+
+ Holds +
+
+ ( + + )} + /> +
+
+ Nav / Track +
+
+ ( + + )} + /> +
+ + } +
+

Notes

+
+
+ ( +