Compare commits
50 Commits
96-switch-
...
v2.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| ee0e10072c | |||
| 899f92f342 | |||
| 5c4245179f | |||
| a7a9f01fd9 | |||
| a367b517db | |||
| fd514e4c8b | |||
| d8513e6dd0 | |||
| 5de5a165da | |||
| 26c46fe6e8 | |||
| 04e5e3e16b | |||
| 0305b424eb | |||
| 8652544471 | |||
| b7b6bdcf4a | |||
| 9c96774f7b | |||
| a9abb5863c | |||
| 477469831f | |||
| 6964531e12 | |||
| 08baaa6d83 | |||
| 487afb08d6 | |||
| b7e74242b8 | |||
| 876f1f65a2 | |||
| 4db1a0fb32 | |||
| f246c1f0f6 | |||
| 36b4018858 | |||
| 6b0c292d34 | |||
| 51de91f8d4 | |||
| 41e9ae33dc | |||
| 870241b37f | |||
| a827b5a39f | |||
| 0663ac873e | |||
| 7cfe005154 | |||
| f421b8d231 | |||
| 5977659dd4 | |||
| 01c1e7015e | |||
| 10d160bbc2 | |||
| f321c2506a | |||
| 976574c6ac | |||
| f3c7fb8853 | |||
| cf8e50fb9c | |||
| fec99878de | |||
| f041e0e1ba | |||
| 64c94ca9f4 | |||
| 4ad53f99c1 | |||
| 819e632d8e | |||
| 5100a93659 | |||
| a4fdd93ede | |||
| 1e1b1f7c97 | |||
| 96b6754f29 | |||
| 952b9a806b | |||
| 42bc48c1df |
@@ -10,7 +10,7 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment_name }}
|
||||
steps:
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
api
|
||||
client
|
||||
|
||||
- name: Install Nest CLI
|
||||
run: |
|
||||
@@ -37,8 +38,19 @@ jobs:
|
||||
run: |
|
||||
npm run build -w api
|
||||
|
||||
- name: Build Client
|
||||
- name: Test API
|
||||
run: |
|
||||
npm run test -w api
|
||||
|
||||
- name: Build Client
|
||||
env:
|
||||
VITE_API_URL: ${{ vars.VITE_API_URL }}
|
||||
VITE_BASE_URL: ${{ vars.VITE_BASE_URL }}
|
||||
VITE_CLIENT_ID: ${{ vars.VITE_CLIENT_ID }}
|
||||
VITE_ISSUER_URI: ${{ vars.VITE_ISSUER_URI }}
|
||||
VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }}
|
||||
run: |
|
||||
printenv
|
||||
npm run build -w client
|
||||
|
||||
- name: Log into Docker Hub
|
||||
@@ -59,4 +71,3 @@ jobs:
|
||||
push: true
|
||||
tags: noahspan/flying:${{ inputs.version_number }}
|
||||
context: .
|
||||
target: api
|
||||
1
.github/workflows/changes.yaml
vendored
1
.github/workflows/changes.yaml
vendored
@@ -20,7 +20,6 @@ jobs:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
initial-fetch-depth: 2
|
||||
filters: |
|
||||
api:
|
||||
- 'api/**'
|
||||
|
||||
6
.github/workflows/deploy.yaml
vendored
6
.github/workflows/deploy.yaml
vendored
@@ -23,13 +23,13 @@ jobs:
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ vars.VITE_CLIENT_ID }}
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant-id: ${{ vars.VITE_TENANT_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
|
||||
- name: Azure CLI script
|
||||
uses: azure/cli@v2
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |
|
||||
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --resource-group noahspan-flying --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
|
||||
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --container-name ${{ inputs.app_name }} --resource-group noahspan --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
|
||||
4
.github/workflows/main.yaml
vendored
4
.github/workflows/main.yaml
vendored
@@ -9,11 +9,11 @@ jobs:
|
||||
uses: ./.github/workflows/changes.yaml
|
||||
|
||||
build:
|
||||
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true' }}
|
||||
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true' }}
|
||||
name: build
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/build.yaml
|
||||
uses: ./.github/workflows/build_and_test.yaml
|
||||
with:
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
|
||||
2
.github/workflows/pull_request.yaml
vendored
2
.github/workflows/pull_request.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
name: build
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/build.yaml
|
||||
uses: ./.github/workflows/build_and_test.yaml
|
||||
with:
|
||||
environment_name: pull_request
|
||||
version_number: ${{ github.run_id }}
|
||||
|
||||
4
.github/workflows/tag.yaml
vendored
4
.github/workflows/tag.yaml
vendored
@@ -2,12 +2,12 @@ name: Tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
uses: ./.github/workflows/build.yaml
|
||||
uses: ./.github/workflows/build_and_test.yaml
|
||||
with:
|
||||
environment_name: prod
|
||||
version_number: ${{ github.ref_name }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:22-slim
|
||||
FROM node:22
|
||||
|
||||
WORKDIR app
|
||||
COPY ./api/dist ./api/dist
|
||||
@@ -9,10 +9,8 @@ WORKDIR api
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /
|
||||
COPY ./api/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["node", "./app/api/dist/main.js"]
|
||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
npx typeorm migration:run -d ./app/api/dist/database/data-source.js
|
||||
node ./app/api/dist/main.js
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "2.0.0-alpha",
|
||||
"version": "2.1.4",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -35,7 +35,7 @@
|
||||
"@nestjs/serve-static": "^5.0.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@noahspan/azure-database": "^3.1.2",
|
||||
"@noahspan/noahspan-modules": "^1.2.9",
|
||||
"@noahspan/noahspan-modules": "^1.2.11",
|
||||
"@schematics/angular": "^17.3.7",
|
||||
"@types/multer": "^1.4.12",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
|
||||
@@ -30,25 +30,26 @@ import { join } from 'path';
|
||||
isGlobal: true,
|
||||
load: [configuration]
|
||||
}),
|
||||
// HealthModule,
|
||||
HealthModule,
|
||||
LogModule,
|
||||
PilotModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '../..', 'client', 'dist')
|
||||
}),
|
||||
TrackModule,
|
||||
TypeOrmModule.forRoot(dataSourceOptions),
|
||||
MsGraphModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
authority: configService.get<string>('authority'),
|
||||
clientId: configService.get<string>('clientId'),
|
||||
clientSecret: configService.get<string>('clientSecret'),
|
||||
tenantId: configService.get<string>('tenantId')
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
PilotModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '../..', 'client', 'dist')
|
||||
}),
|
||||
TrackModule,
|
||||
TypeOrmModule.forRoot(dataSourceOptions)
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default () => ({
|
||||
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||
audience: process.env.AUDIENCE,
|
||||
authority: process.env.AUTHORITY,
|
||||
clientId: process.env.CLIENT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
issuer: process.env.ISSUER_URL,
|
||||
|
||||
@@ -11,7 +11,8 @@ export const dataSourceOptions: DataSourceOptions = {
|
||||
database: configService.get<string>('DB_PATH'),
|
||||
entities: ['../**/*.entity.js'],
|
||||
migrations: ['./migrations/*.js'],
|
||||
synchronize: configService.get<boolean>('DB_SYNC')
|
||||
synchronize: configService.get<boolean>('DB_SYNC'),
|
||||
migrationsRun: true
|
||||
}
|
||||
|
||||
const dataSource = new DataSource(dataSourceOptions);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'endorsements' })
|
||||
|
||||
@@ -48,10 +48,10 @@ import { ConfigService } from '@nestjs/config';
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
async downloadFile(containerName: string, rowKey: string, fileName: string): Promise<string> {
|
||||
async downloadFile(containerName: string, logId: string, fileName: string): Promise<string> {
|
||||
this.containerName = containerName;
|
||||
|
||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
|
||||
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
||||
const downloadBlockBlobResponse = await blockBlobClient.download();
|
||||
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
|
||||
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { HealthController } from "./health.controller"
|
||||
import { HttpStatus } from "@nestjs/common";
|
||||
|
||||
describe('HealthController', () => {
|
||||
let controller; HealthController;
|
||||
|
||||
const mockHealthService = {
|
||||
isDatabaseConnected: jest.fn()
|
||||
}
|
||||
let controller: HealthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [HealthController],
|
||||
providers: [HealthService]
|
||||
controllers: [HealthController]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<HealthController>(HealthController);
|
||||
})
|
||||
|
||||
it('isHealthy => should return true', () => {
|
||||
it('should be defined', () => {
|
||||
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);
|
||||
|
||||
it('isHealth => should return status ok', async () => {
|
||||
const result = await controller.isHealthy();
|
||||
|
||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||
expect(result).toEqual(false);
|
||||
expect(result).toEqual(HttpStatus.OK);
|
||||
})
|
||||
})
|
||||
@@ -1,27 +1,13 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { HealthService } from './health.service';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly healthService: HealthService
|
||||
) {}
|
||||
|
||||
|
||||
@Get()
|
||||
async isHealthy(): Promise<boolean> {
|
||||
try {
|
||||
return await this.healthService.isDatabaseConnected();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
async isHealthy(): Promise<HttpStatus> {
|
||||
return HttpStatus.OK
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
HealthService
|
||||
]
|
||||
})
|
||||
export class HealthModule {}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
describe('HealthService', () => {
|
||||
let service: HealthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [HealthService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<HealthService>(HealthService);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async isDatabaseConnected(): Promise<boolean> {
|
||||
try {
|
||||
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
|
||||
|
||||
if (isDatabaseConnected) {
|
||||
return isDatabaseConnected;
|
||||
} else {
|
||||
throw new CustomError('Database not connected', 'Database not connected', 400)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { JwtPayload } from "jwt-decode";
|
||||
|
||||
export interface CustomJwtPayload extends JwtPayload {
|
||||
roles: string[];
|
||||
}
|
||||
449
api/src/log/log.controller.spec.ts
Normal file
449
api/src/log/log.controller.spec.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LogController } from './log.controller';
|
||||
import { LogService } from './log.service';
|
||||
import { LogDto } from './log.dto';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('LogController', () => {
|
||||
let controller: LogController;
|
||||
|
||||
const mockLogService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findLogsWithCount: jest.fn(),
|
||||
findLogsWithTracks: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [LogController],
|
||||
providers: [
|
||||
ConfigService,
|
||||
FileService,
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mockLogService
|
||||
}
|
||||
]
|
||||
}).compile()
|
||||
|
||||
controller = module.get<LogController>(LogController);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
})
|
||||
|
||||
it('find => should find a log by id', async () => {
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(log);
|
||||
|
||||
const result = await controller.find(log.id);
|
||||
|
||||
expect(result).toEqual(log);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(log.id);
|
||||
})
|
||||
|
||||
it('find => should fail to find a log by id', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockRejectedValue(new Error('Log not found'))
|
||||
|
||||
try {
|
||||
await controller.find(id);
|
||||
|
||||
fail('find did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(id);
|
||||
expect(mockLogService.find).rejects.toThrow('Log not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should find logs with count', async () => {
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log]
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockLogService, 'findLogsWithCount').mockReturnValue({
|
||||
entities: logs,
|
||||
total: count,
|
||||
hasNextPage: morePages
|
||||
});
|
||||
|
||||
const {entities, total, hasNextPage} = await controller.findLogsWithCount();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(total).toEqual(count);
|
||||
expect(hasNextPage).toEqual(morePages)
|
||||
expect(mockLogService.findLogsWithCount).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should fail to find logs with count', async () => {
|
||||
jest.spyOn(mockLogService, 'findLogsWithCount').mockRejectedValue(new Error('Logs not found'))
|
||||
|
||||
try {
|
||||
await controller.findLogsWithCount();
|
||||
|
||||
fail('findLogsWithCount did not throw error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.findLogsWithCount).toHaveBeenCalled();
|
||||
expect(mockLogService.findLogsWithCount).rejects.toThrow('Logs not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('findLogsWithTracks => should find logs with tracks', async () => {
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log]
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockLogService, 'findLogsWithTracks').mockReturnValue({
|
||||
entities: logs,
|
||||
total: count,
|
||||
hasNextPage: morePages
|
||||
});
|
||||
|
||||
const {entities, total, hasNextPage} = await controller.findLogsWithTracks();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(total).toEqual(count);
|
||||
expect(hasNextPage).toEqual(morePages)
|
||||
expect(mockLogService.findLogsWithTracks).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should fail to find logs with tracks', async () => {
|
||||
jest.spyOn(mockLogService, 'findLogsWithTracks').mockRejectedValue(new Error('Logs not found'))
|
||||
|
||||
try {
|
||||
await controller.findLogsWithTracks();
|
||||
|
||||
fail('findLogsWithTracks did not throw error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.findLogsWithTracks).toHaveBeenCalled();
|
||||
expect(mockLogService.findLogsWithTracks).rejects.toThrow('Logs not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('create => should create a new log', async () => {
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto;
|
||||
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockLogService, 'create').mockReturnValue(log);
|
||||
|
||||
const result = await controller.create(logDto);
|
||||
|
||||
expect(mockLogService.create).toHaveBeenCalled();
|
||||
expect(mockLogService.create).toHaveBeenCalledWith(logDto);
|
||||
expect(result).toEqual(log);
|
||||
})
|
||||
|
||||
it('create => should fail to create a new log', async () => {
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto;
|
||||
|
||||
jest.spyOn(mockLogService, 'create').mockRejectedValue(new Error('Log failed to create'))
|
||||
|
||||
try {
|
||||
await controller.create(logDto);
|
||||
|
||||
fail('create did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.create).toHaveBeenCalled();
|
||||
expect(mockLogService.create).toHaveBeenCalledWith(logDto);
|
||||
expect(mockLogService.create).rejects.toThrow('Log failed to create')
|
||||
}
|
||||
})
|
||||
|
||||
it('update => should update an existing log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto
|
||||
|
||||
jest.spyOn(mockLogService, 'update').mockReturnValue(logDto);
|
||||
|
||||
const result = await controller.update(id, logDto);
|
||||
|
||||
expect(result).toEqual(logDto);
|
||||
expect(mockLogService.update).toHaveBeenCalled();
|
||||
expect(mockLogService.update).toHaveBeenCalledWith(id, logDto);
|
||||
})
|
||||
|
||||
it('update => should fail to update an exising log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto;
|
||||
|
||||
jest.spyOn(mockLogService, 'update').mockRejectedValue(new Error('Log failed to update'))
|
||||
|
||||
try {
|
||||
await controller.update(id, logDto);
|
||||
|
||||
fail('update did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.update).toHaveBeenCalled();
|
||||
expect(mockLogService.update).toHaveBeenCalledWith(id, logDto);
|
||||
expect(mockLogService.update).rejects.toThrow('Log failed to update')
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete and existing log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
|
||||
jest.spyOn(mockLogService, 'delete');
|
||||
|
||||
const result = await controller.delete(id);
|
||||
|
||||
expect(mockLogService.delete).toHaveBeenCalled();
|
||||
expect(mockLogService.delete).toHaveBeenCalledWith(id);
|
||||
})
|
||||
|
||||
it('delete => should fail to delete and exising log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
|
||||
jest.spyOn(mockLogService, 'delete').mockRejectedValue(new Error('Log failed to delete'));
|
||||
|
||||
try {
|
||||
await controller.delete(id);
|
||||
|
||||
fail('delete did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.delete).toHaveBeenCalled();
|
||||
expect(mockLogService.delete).toHaveBeenCalledWith(id);
|
||||
expect(mockLogService.delete).rejects.toThrow('Log failed to delete')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
@@ -19,6 +20,7 @@ import { LogInterceptor } from './log.interceptor';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Logs } from './logs.interface';
|
||||
|
||||
const reflector = new Reflector();
|
||||
|
||||
@@ -30,6 +32,29 @@ export class LogController {
|
||||
private readonly logService: LogService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findLogsWithCount(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
|
||||
try {
|
||||
return await this.logService.findLogsWithCount(skip, take)
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@Get('flights')
|
||||
@Public()
|
||||
async findLogsWithTracks(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
|
||||
try {
|
||||
return await this.logService.findLogsWithTracks(skip, take)
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Public()
|
||||
@@ -37,37 +62,22 @@ export class LogController {
|
||||
@Param('id') id: string,
|
||||
): Promise<LogEntity> {
|
||||
try {
|
||||
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()
|
||||
@Public()
|
||||
async findAll(): Promise<LogEntity[]> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
||||
async create(@Body() logDto: LogDto) {
|
||||
try {
|
||||
return await this.logService.create(logDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
console.log(error)
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
@@ -77,14 +87,12 @@ export class LogController {
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() logDto: LogDto
|
||||
): Promise<UpdateResult> {
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +105,6 @@ export class LogController {
|
||||
try {
|
||||
return await this.logService.delete(id);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { PilotEntity } from "src/pilot/pilot.entity";
|
||||
|
||||
export class LogDto {
|
||||
pilotId: string;
|
||||
date: Date;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { TrackEntity } from 'src/track/track.entity';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { TrackEntity } from '../track/track.entity';
|
||||
import { ColumnNumericTransformer } from '../transformers/columnNumeric.transformer';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'logs' })
|
||||
@@ -25,52 +26,131 @@ export class LogEntity {
|
||||
@Column()
|
||||
routeTo: string;
|
||||
|
||||
@Column()
|
||||
@Column('numeric', {
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer()
|
||||
})
|
||||
durationOfFlight: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
singleEngineLand: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
simulatorAtd: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
landingsDay: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
landingsNight: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
groundTrainingReceived: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
flightTrainingReceived: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
crossCountry: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
night: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
solo: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
pilotInCommand: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentActual: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentSimulated: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentApproaches: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentHolds: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentNavTrack: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException }
|
||||
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';
|
||||
|
||||
@@ -16,42 +15,62 @@ export class LogInterceptor implements NestInterceptor {
|
||||
]);
|
||||
|
||||
return handler.handle().pipe(
|
||||
map((data: LogEntity[]) => {
|
||||
map((data: any) => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
const limitData = (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);
|
||||
const jwtPayload = jwtDecode(token);
|
||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
||||
|
||||
if (jwtPayload.roles.includes('Flying.Read')) {
|
||||
const logs = limitData(data);
|
||||
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
||||
if (data.entities) {
|
||||
const logs = data.entities.map((entity) => limitData(data.entities));
|
||||
|
||||
return {
|
||||
entities: logs,
|
||||
total: data.total,
|
||||
hasNextPage: data.hasNextPage
|
||||
};
|
||||
} else {
|
||||
const log = limitData(data);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
return logs;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
} else if (!req.headers.authorization && isPublic) {
|
||||
const publicData = limitData(data)
|
||||
const logs = publicData.slice(0, 5)
|
||||
if (data.entities) {
|
||||
const publicData = data.entities.map((entity) => limitData(entity))
|
||||
const logs = publicData.slice(0, 5)
|
||||
|
||||
return logs;
|
||||
return {
|
||||
entities: logs,
|
||||
total: logs.length,
|
||||
hasNextPage: false
|
||||
};
|
||||
} else {
|
||||
const publicData = limitData(data);
|
||||
|
||||
return publicData
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ 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';
|
||||
import { PilotModule } from '../pilot/pilot.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
366
api/src/log/log.service.spec.ts
Normal file
366
api/src/log/log.service.spec.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LogService } from './log.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { LogDto } from './log.dto';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PilotService } from '../pilot/pilot.service';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
|
||||
describe('LogService', () => {
|
||||
let service: LogService;
|
||||
|
||||
const mockFileService = {
|
||||
deleteFolder: jest.fn()
|
||||
}
|
||||
|
||||
const mockQueryBuilder = {
|
||||
createQueryBuilder: jest.fn().mockReturnThis(),
|
||||
innerJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn()
|
||||
}
|
||||
|
||||
const mockLogRepository = {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||
delete: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findOneBy: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}
|
||||
|
||||
const mockPilotRepository = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ConfigService,
|
||||
LogService,
|
||||
PilotService,
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: mockFileService
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(LogEntity),
|
||||
useValue: mockLogRepository
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(PilotEntity),
|
||||
useValue: mockPilotRepository
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
service = module.get<LogService>(LogService);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
})
|
||||
|
||||
it('create => should create a log entry', async () => {
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto
|
||||
|
||||
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
|
||||
|
||||
const result = await service.create(logDto);
|
||||
|
||||
expect(mockLogRepository.save).toHaveBeenCalled();
|
||||
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
|
||||
})
|
||||
|
||||
it('delete => should delete a log entry', async () => {
|
||||
const id: string = '';
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockFileService, 'deleteFolder').mockReturnValue(undefined);
|
||||
jest.spyOn(mockLogRepository, 'delete').mockReturnValue(log);
|
||||
|
||||
const result = await service.delete(id);
|
||||
|
||||
expect(result).toEqual(log);
|
||||
expect(mockLogRepository.delete).toHaveBeenCalled();
|
||||
expect(mockLogRepository.delete).toHaveBeenCalledWith({ id: id })
|
||||
})
|
||||
|
||||
it('find => should find a log entry by id', async () => {
|
||||
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockLogRepository, 'findOne').mockReturnValue(log);
|
||||
|
||||
const result = await service.find(id);
|
||||
|
||||
expect(result).toEqual(log);
|
||||
expect(mockLogRepository.findOne).toHaveBeenCalled();
|
||||
expect(mockLogRepository.findOne).toHaveBeenCalledWith({
|
||||
relations: [
|
||||
'pilot',
|
||||
'tracks'
|
||||
],
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should find log entries with count', async () => {
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log];
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockQueryBuilder, 'getManyAndCount').mockReturnValue([logs, count, morePages]);
|
||||
|
||||
const {entities, total, hasNextPage} = await service.findLogsWithCount();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(count).toEqual(total);
|
||||
expect(hasNextPage).toEqual(morePages);
|
||||
expect(mockQueryBuilder.getManyAndCount).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findLogsWithTracks => should find log entries with tracks', async () => {
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log];
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockQueryBuilder, 'getManyAndCount').mockResolvedValue([logs, count, morePages])
|
||||
|
||||
const {entities, total, hasNextPage} = await service.findLogsWithTracks();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(count).toEqual(total);
|
||||
expect(hasNextPage).toEqual(morePages);
|
||||
expect(mockQueryBuilder.getManyAndCount).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('update => should update a log entry', async () => {
|
||||
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockLogRepository, 'findOneBy').mockReturnValue(log);
|
||||
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
|
||||
|
||||
const result = await service.update(id, logDto);
|
||||
|
||||
expect(result).toEqual(logDto);
|
||||
expect(mockLogRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockLogRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
expect(mockLogRepository.save).toHaveBeenCalled();
|
||||
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,11 @@ 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';
|
||||
import { PilotService } from '../pilot/pilot.service';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { Logs } from './logs.interface';
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
@@ -17,42 +18,65 @@ export class LogService {
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<LogEntity> {
|
||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
||||
return await this.logRepository.findOne({
|
||||
where: { id: id },
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
|
||||
return logEntity;
|
||||
}
|
||||
|
||||
async findAll(): Promise<LogEntity[]> {
|
||||
return await this.logRepository.find({
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
}
|
||||
async findLogsWithCount(skip?: number, take?: number): Promise<Logs> {
|
||||
// const [entities, total] = await this.logRepository.findAndCount({
|
||||
// take,
|
||||
// skip,
|
||||
// order: {
|
||||
// date: 'DESC'
|
||||
// },
|
||||
// relations: ['pilot', 'tracks']
|
||||
// })
|
||||
|
||||
async create(logDto: LogDto): Promise<InsertResult> {
|
||||
try{
|
||||
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
|
||||
const [entities, total] = await this.logRepository
|
||||
.createQueryBuilder('logs')
|
||||
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||
.orderBy('logs.date', 'DESC')
|
||||
.skip(skip)
|
||||
.take(take)
|
||||
.getManyAndCount()
|
||||
|
||||
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
|
||||
return {
|
||||
entities,
|
||||
total,
|
||||
hasNextPage: skip + take < total
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, log: LogDto): Promise<UpdateResult> {
|
||||
return await this.logRepository.update(id, log);
|
||||
async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
|
||||
const [entities, total] = await this.logRepository
|
||||
.createQueryBuilder('logs')
|
||||
.innerJoinAndSelect('logs.tracks', 'track')
|
||||
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||
.orderBy('logs.date', 'DESC')
|
||||
.skip(skip)
|
||||
.take(take)
|
||||
.getManyAndCount();
|
||||
|
||||
return {
|
||||
entities,
|
||||
total,
|
||||
hasNextPage: skip + take < total
|
||||
}
|
||||
}
|
||||
|
||||
async create(logDto: LogDto): Promise<LogDto> {
|
||||
return this.logRepository.save(logDto);
|
||||
}
|
||||
|
||||
async update(id: string, logDto: LogDto): Promise<LogDto> {
|
||||
const logEntity: LogEntity = await this.logRepository.findOneBy({ id });
|
||||
const logEntityUpdated = Object.assign(logEntity, logDto)
|
||||
|
||||
delete logEntityUpdated.tracks;
|
||||
|
||||
return await this.logRepository.save(logEntityUpdated);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
|
||||
7
api/src/log/logs.interface.ts
Normal file
7
api/src/log/logs.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
|
||||
export interface Logs {
|
||||
entities: LogEntity[],
|
||||
total: number,
|
||||
hasNextPage: boolean
|
||||
}
|
||||
@@ -9,16 +9,12 @@ async function bootstrap() {
|
||||
const httpService = new HttpService();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
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.enableCors();
|
||||
app.setGlobalPrefix('api');
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.use(
|
||||
session({
|
||||
secret: 'blah',
|
||||
secret: process.env.SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'medical' })
|
||||
|
||||
260
api/src/pilot/pilot.controller.spec.ts
Normal file
260
api/src/pilot/pilot.controller.spec.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PilotController } from "./pilot.controller";
|
||||
import { PilotService } from './pilot.service';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { HttpException } from '@nestjs/common';
|
||||
|
||||
describe('PilotController', () => {
|
||||
let controller: PilotController;
|
||||
|
||||
const mockPilotService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PilotController],
|
||||
providers: [
|
||||
{
|
||||
provide: PilotService,
|
||||
useValue: mockPilotService
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PilotController>(PilotController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('find => should find a pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
|
||||
jest.spyOn(mockPilotService, 'find').mockReturnValue(pilot);
|
||||
|
||||
const result = await controller.find(id);
|
||||
|
||||
expect(result).toEqual(pilot);
|
||||
expect(mockPilotService.find).toHaveBeenCalled();
|
||||
expect(mockPilotService.find).toHaveBeenCalledWith(id)
|
||||
})
|
||||
|
||||
|
||||
it('find => should fail to find a pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
|
||||
jest.spyOn(mockPilotService, 'find').mockRejectedValue(new Error('Pilot not found'));
|
||||
|
||||
try {
|
||||
await controller.find(id);
|
||||
|
||||
fail('find did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.find).toHaveBeenCalled();
|
||||
expect(mockPilotService.find).toHaveBeenCalledWith(id);
|
||||
expect(mockPilotService.find).rejects.toThrow('Pilot not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('findAll => should find all pilots', async () => {
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
const pilots = [pilot];
|
||||
|
||||
jest.spyOn(mockPilotService, 'findAll').mockReturnValue(pilots);
|
||||
|
||||
const result = await controller.findAll();
|
||||
|
||||
expect(result).toEqual(pilots);
|
||||
expect(mockPilotService.findAll).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findAll => should fail to find all pilots', async () => {
|
||||
jest.spyOn(mockPilotService, 'findAll').mockRejectedValue(new Error('Pilots not found'));
|
||||
|
||||
try {
|
||||
await controller.findAll();
|
||||
|
||||
fail('findAll did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.findAll).toHaveBeenCalled();
|
||||
expect(mockPilotService.findAll).rejects.toThrow('Pilots not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('create => should create a new pilot', async () => {
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotService, 'create').mockReturnValue(pilot);
|
||||
|
||||
const result = await controller.create(pilotDto);
|
||||
|
||||
expect(mockPilotService.create).toHaveBeenCalled();
|
||||
expect(mockPilotService.create).toHaveBeenCalledWith(pilotDto)
|
||||
expect(result).toEqual(pilot);
|
||||
})
|
||||
|
||||
it('create => should fail to create a new pilot', async () => {
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
jest.spyOn(mockPilotService, 'create').mockRejectedValue(new Error('Pilot failed to create'));
|
||||
|
||||
try {
|
||||
await controller.create(pilotDto);
|
||||
|
||||
fail('create did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.create).toHaveBeenCalled();
|
||||
expect(mockPilotService.create).toHaveBeenCalledWith(pilotDto);
|
||||
expect(mockPilotService.create).rejects.toThrow('Pilot failed to create')
|
||||
}
|
||||
})
|
||||
|
||||
it('update => should update an existing pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilotDto = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
jest.spyOn(mockPilotService, 'update').mockReturnValue(pilotDto);
|
||||
|
||||
const result = await controller.update(id, pilotDto);
|
||||
|
||||
expect(result).toEqual(pilotDto);
|
||||
expect(mockPilotService.update).toHaveBeenCalled();
|
||||
expect(mockPilotService.update).toHaveBeenCalledWith(id, pilotDto)
|
||||
})
|
||||
|
||||
it('update => should fail to update an existing pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilotDto = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
jest.spyOn(mockPilotService, 'update').mockRejectedValue(new Error('Pilot failed to update'))
|
||||
|
||||
try {
|
||||
await controller.update(id, pilotDto);
|
||||
|
||||
fail('update function did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.update).toHaveBeenCalled();
|
||||
expect(mockPilotService.update).toHaveBeenCalledWith(id, pilotDto);
|
||||
expect(mockPilotService.update).rejects.toThrow('Pilot failed to update')
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete an existing pilot', async () => {
|
||||
const id = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
|
||||
|
||||
jest.spyOn(mockPilotService, 'delete');
|
||||
|
||||
const result = await controller.delete(id);
|
||||
|
||||
expect(mockPilotService.delete).toHaveBeenCalled();
|
||||
expect(mockPilotService.delete).toHaveBeenCalledWith(id);
|
||||
})
|
||||
|
||||
it('delete => should fail to delete an existing pilot', async () => {
|
||||
const id = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
|
||||
|
||||
jest.spyOn(mockPilotService, 'delete').mockRejectedValue(new Error('Pilot failed to delete'));
|
||||
|
||||
try {
|
||||
await controller.delete(id);
|
||||
|
||||
fail('delete function did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.delete).toHaveBeenCalled();
|
||||
expect(mockPilotService.delete).toHaveBeenCalledWith(id);
|
||||
expect(mockPilotService.delete).rejects.toThrow('Pilot failed to delete')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -43,22 +43,20 @@ export class PilotController {
|
||||
return await this.pilotService.findAll();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
console.log(error)
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() pilotDto: PilotDto) {
|
||||
async create(@Body() pilotDto: PilotDto): Promise<PilotDto> {
|
||||
try {
|
||||
|
||||
return await this.pilotService.create(pilotDto);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
// const customError = error as CustomError;
|
||||
const customError = error as CustomError;
|
||||
|
||||
// throw new HttpException(customError.message, customError.statusCode);
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
|
||||
export class PilotDto {
|
||||
name: string;
|
||||
address: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LogEntity } from 'src/log/log.entity';
|
||||
import { LogEntity } from '../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';
|
||||
import { EndorsementEntity } from '../endorsement/endorsement.entity';
|
||||
import { MedicalEntity } from '../medical/medical.entity';
|
||||
|
||||
@Entity({ name: 'pilots' })
|
||||
export class PilotEntity {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -26,13 +25,15 @@ export class PilotInterceptor implements NestInterceptor {
|
||||
};
|
||||
})
|
||||
}
|
||||
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 jwtPayload = jwtDecode(token);
|
||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
||||
|
||||
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
||||
const pilots = limitData(data)
|
||||
|
||||
return pilots;
|
||||
|
||||
206
api/src/pilot/pilot.service.spec.ts
Normal file
206
api/src/pilot/pilot.service.spec.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PilotService } from './pilot.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { CustomError } from '../error/customError';
|
||||
|
||||
describe('PilotService', () => {
|
||||
let service: PilotService;
|
||||
|
||||
const mockPilotRepository = {
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOneBy: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PilotService,
|
||||
{
|
||||
provide: getRepositoryToken(PilotEntity),
|
||||
useValue: mockPilotRepository
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
service = module.get<PilotService>(PilotService);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
})
|
||||
|
||||
it('find => should find one pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(pilot);
|
||||
|
||||
const result = await service.find(id);
|
||||
|
||||
expect(result).toEqual(pilot);
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
})
|
||||
|
||||
it('find => should fail to find one pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
|
||||
const mockCustomError = new CustomError('Pilot not found', 'Not found', 404)
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(undefined);
|
||||
|
||||
try {
|
||||
await service.find(id);
|
||||
|
||||
fail('find did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CustomError);
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
}
|
||||
})
|
||||
|
||||
it('findAll => should find all pilots', async () => {
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
const pilots = [pilot];
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'find').mockReturnValue(pilots);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual(pilots);
|
||||
expect(mockPilotRepository.find).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('create => should create a new pilot', async () => {
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'save').mockReturnValue(pilot);
|
||||
|
||||
const result = await service.create(pilotDto);
|
||||
|
||||
expect(mockPilotRepository.save).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.save).toHaveBeenCalledWith(pilotDto);
|
||||
})
|
||||
|
||||
it('update => should update a pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(pilot)
|
||||
jest.spyOn(mockPilotRepository, 'save').mockReturnValue(pilotDto);
|
||||
|
||||
const result = await service.update(id, pilotDto)
|
||||
|
||||
expect(result).toEqual(pilotDto);
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
expect(mockPilotRepository.save).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.save).toHaveBeenCalledWith(pilotDto)
|
||||
})
|
||||
|
||||
it('delete => should delete a pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'delete').mockReturnValue(pilot);
|
||||
|
||||
const result = await service.delete(id);
|
||||
|
||||
expect(result).toEqual(pilot);
|
||||
expect(mockPilotRepository.delete).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.delete).toHaveBeenCalledWith({ id: id });
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,7 @@ 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';
|
||||
import { CustomError } from '../error/customError';
|
||||
|
||||
@Injectable()
|
||||
export class PilotService {
|
||||
@@ -13,7 +13,7 @@ export class PilotService {
|
||||
|
||||
async find(id: string): Promise<PilotEntity> {
|
||||
try {
|
||||
const pilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||
|
||||
if (pilotEntity) {
|
||||
return pilotEntity
|
||||
@@ -29,15 +29,18 @@ export class PilotService {
|
||||
return await this.pilotRepository.find();
|
||||
}
|
||||
|
||||
async create(pilot: PilotDto): Promise<InsertResult> {
|
||||
return await this.pilotRepository.insert(pilot);
|
||||
async create(pilot: PilotDto): Promise<PilotDto> {
|
||||
return await this.pilotRepository.save(pilot);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
pilot: PilotDto
|
||||
): Promise<UpdateResult> {
|
||||
return await this.pilotRepository.update(id, pilot);
|
||||
pilotDto: PilotDto
|
||||
): Promise<PilotDto> {
|
||||
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id })
|
||||
const pilotEntityUpdated = Object.assign(pilotEntity, pilotDto)
|
||||
|
||||
return await this.pilotRepository.save(pilotEntityUpdated);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
|
||||
218
api/src/track/track.controller.spec.ts
Normal file
218
api/src/track/track.controller.spec.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TrackController } from './track.controller';
|
||||
import { TrackService } from './track.service';
|
||||
import { TrackDto } from './track.dto';
|
||||
import { TrackEntity } from './track.entity';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { HttpException } from '@nestjs/common';
|
||||
import { Readable } from 'stream';
|
||||
import { DeleteResult, InsertResult } from 'typeorm';
|
||||
|
||||
describe('TrackController', () => {
|
||||
let controller: TrackController;
|
||||
|
||||
const mockTrackService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
downloadTrackFile: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TrackController],
|
||||
providers: [
|
||||
ConfigService,
|
||||
FileService,
|
||||
{
|
||||
provide: TrackService,
|
||||
useValue: mockTrackService
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<TrackController>(TrackController);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
})
|
||||
|
||||
it('findAll => should find all tracks by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity
|
||||
const tracks = [track]
|
||||
|
||||
jest.spyOn(mockTrackService, 'findAll').mockReturnValue(tracks);
|
||||
|
||||
const result = await controller.findAll(logId);
|
||||
|
||||
expect(result).toEqual(tracks);
|
||||
expect(mockTrackService.findAll).toHaveBeenCalled();
|
||||
expect(mockTrackService.findAll).toHaveBeenLastCalledWith(logId);
|
||||
})
|
||||
|
||||
it('findAll => should fail to find all tracks by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
|
||||
jest.spyOn(mockTrackService, 'findAll').mockRejectedValue(new Error('Tracks not found'))
|
||||
|
||||
try {
|
||||
await controller.findAll(logId);
|
||||
|
||||
fail('findAll did not throw error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.findAll).toHaveBeenCalled();
|
||||
expect(mockTrackService.findAll).rejects.toThrow('Tracks not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('create => create a track by a log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
const mockInsertResult: InsertResult = {
|
||||
identifiers: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
}
|
||||
],
|
||||
generatedMaps: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
createdAd: new Date()
|
||||
}
|
||||
],
|
||||
raw: {
|
||||
affectedRows: 1
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackService, 'create').mockReturnValue(mockInsertResult);
|
||||
|
||||
const result = await controller.create(logId, order, file);
|
||||
|
||||
expect(result).toEqual(mockInsertResult);
|
||||
expect(mockTrackService.create).toHaveBeenCalled();
|
||||
expect(mockTrackService.create).toHaveBeenCalledWith(logId, order, file)
|
||||
})
|
||||
|
||||
it('create => should fail to create a track by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackService, 'create').mockRejectedValue(new Error('Track failed to create'))
|
||||
|
||||
try {
|
||||
await controller.create(logId, order, file)
|
||||
|
||||
fail('create did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.create).toHaveBeenCalled();
|
||||
expect(mockTrackService.create).toHaveBeenCalledWith(logId, order, file);
|
||||
expect(mockTrackService.create).rejects.toThrow('Track failed to create')
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete a track by log id', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
const mockDeleteResult: DeleteResult ={
|
||||
raw: [],
|
||||
affected: 1
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackService, 'delete').mockReturnValue(mockDeleteResult);
|
||||
|
||||
const result = await controller.delete(id, fileName, logId);
|
||||
|
||||
expect(result).toEqual(mockDeleteResult);
|
||||
expect(mockTrackService.delete).toHaveBeenCalled();
|
||||
expect(mockTrackService.delete).toHaveBeenCalledWith(id, logId, fileName);
|
||||
|
||||
})
|
||||
|
||||
it('delete => should fail to delete a track by log id', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
|
||||
jest.spyOn(mockTrackService, 'delete').mockRejectedValue(new Error('Track failed to delete'));
|
||||
|
||||
try {
|
||||
await controller.delete(id, fileName, logId);
|
||||
|
||||
fail('delete did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.delete).toHaveBeenCalled();
|
||||
expect(mockTrackService.delete).toHaveBeenCalledWith(id, logId, fileName);
|
||||
expect(mockTrackService.delete).rejects.toThrow('Track failed to delete')
|
||||
}
|
||||
})
|
||||
|
||||
it('downloadTrack => should download a track by log id', async () => {
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
const mockStreamToBufferString = 'fake-stream-to-buffer-string'
|
||||
|
||||
jest.spyOn(mockTrackService, 'downloadTrackFile').mockReturnValue(mockStreamToBufferString);
|
||||
|
||||
const result = await controller.downloadTrack(logId, fileName);
|
||||
|
||||
expect(result).toEqual(mockStreamToBufferString);
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalled();
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalledWith(logId, fileName)
|
||||
})
|
||||
|
||||
it('downloadTrack => should fail to download a track by log id', async () => {
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
|
||||
jest.spyOn(mockTrackService, 'downloadTrackFile').mockRejectedValue(new Error('Track failed to download'));
|
||||
|
||||
try {
|
||||
await controller.downloadTrack(logId, fileName);
|
||||
|
||||
fail('downloadTrack did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalled();
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalledWith(logId, fileName);
|
||||
expect(mockTrackService.downloadTrackFile).rejects.toThrow('Track failed to download')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogEntity } from 'src/log/log.entity';
|
||||
import { LogEntity } from '../log/log.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tracks' })
|
||||
|
||||
@@ -4,7 +4,7 @@ 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';
|
||||
import { LogModule } from '../log/log.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
346
api/src/track/track.service.spec.ts
Normal file
346
api/src/track/track.service.spec.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TrackService } from './track.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { TrackEntity } from './track.entity'
|
||||
import { TrackDto } from './track.dto';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { LogService } from '../log/log.service';
|
||||
import { LogEntity } from '../log/log.entity';
|
||||
import { PilotService } from '../pilot/pilot.service';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { Readable } from 'stream';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { CustomError } from '../error/customError';
|
||||
|
||||
describe('TrackService', () => {
|
||||
let service: TrackService;
|
||||
|
||||
const mockFileService = {
|
||||
deleteFile: jest.fn(),
|
||||
downloadFile: jest.fn(),
|
||||
uploadFile: jest.fn()
|
||||
}
|
||||
|
||||
const mockLogService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
const mockPilotRepository = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
const mockTrackRepository = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
downloadTrackFile: jest.fn(),
|
||||
findOneBy: jest.fn(),
|
||||
find: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ConfigService,
|
||||
LogService,
|
||||
PilotService,
|
||||
TrackService,
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: mockFileService
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mockLogService
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(PilotEntity),
|
||||
useValue: mockPilotRepository
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(TrackEntity),
|
||||
useValue: mockTrackRepository
|
||||
}
|
||||
]
|
||||
}).compile()
|
||||
|
||||
service = module.get<TrackService>(TrackService);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
})
|
||||
|
||||
it('create => should upload a track file', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const log = {
|
||||
id: '094ec69c-72c4-4995-8821-79d5b79bedda',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
const mockInsertResult: InsertResult = {
|
||||
identifiers: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
}
|
||||
],
|
||||
generatedMaps: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
createdAd: new Date()
|
||||
}
|
||||
],
|
||||
raw: {
|
||||
affectedRows: 1
|
||||
}
|
||||
}
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(log)
|
||||
jest.spyOn(mockTrackRepository, 'create').mockReturnValue(track)
|
||||
jest.spyOn(mockTrackRepository, 'insert').mockReturnValue(mockInsertResult);
|
||||
|
||||
const result = await service.create(logId, order, file);
|
||||
|
||||
expect(result).toEqual(mockInsertResult)
|
||||
expect(mockTrackRepository.insert).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.insert).toHaveBeenCalledWith({
|
||||
id: track.id,
|
||||
order: track.order,
|
||||
url: track.url
|
||||
})
|
||||
})
|
||||
|
||||
it('create => should fail to find log', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(undefined);
|
||||
|
||||
try {
|
||||
await service.create(logId, order, file);
|
||||
|
||||
fail('find failed to throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CustomError);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(logId);
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete a track file', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const fileName = 'test_track.kml'
|
||||
const mockDeleteResult: DeleteResult ={
|
||||
raw: [],
|
||||
affected: 1
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackRepository, 'delete').mockReturnValue(mockDeleteResult);
|
||||
|
||||
const result = await service.delete(id, logId, fileName)
|
||||
|
||||
expect(result).toEqual(mockDeleteResult);
|
||||
expect(mockTrackRepository.delete).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.delete).toHaveBeenCalledWith({ id: id })
|
||||
})
|
||||
|
||||
it('delete => should fail to delete file in file service', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const fileName = 'test_track.kml'
|
||||
|
||||
jest.spyOn(mockFileService, 'deleteFile').mockRejectedValue(new Error('Failed to delete file'))
|
||||
|
||||
try {
|
||||
await service.delete(id, logId, fileName)
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockFileService.deleteFile).toHaveBeenCalled();
|
||||
expect(mockFileService.deleteFile).toHaveBeenCalledWith('tracks', logId, fileName)
|
||||
expect(mockFileService.deleteFile).rejects.toThrow('Failed to delete file')
|
||||
}
|
||||
})
|
||||
|
||||
it('downloadTrackFile => should download a track file by log id and filename', async () => {
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
const mockStreamToBufferString = 'fake-stream-to-buffer-string'
|
||||
|
||||
jest.spyOn(mockFileService, 'downloadFile').mockReturnValue(mockStreamToBufferString);
|
||||
|
||||
const result = await service.downloadTrackFile(logId, fileName);
|
||||
|
||||
expect(result).toEqual(mockStreamToBufferString);
|
||||
expect(mockFileService.downloadFile).toHaveBeenCalled();
|
||||
expect(mockFileService.downloadFile).toHaveBeenCalledWith('tracks', logId, fileName);
|
||||
})
|
||||
|
||||
it('find => should find a track file by id', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity
|
||||
|
||||
jest.spyOn(mockTrackRepository, 'findOneBy').mockReturnValue(track)
|
||||
|
||||
const result = await service.find(id);
|
||||
|
||||
expect(result).toEqual(track);
|
||||
expect(mockTrackRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.findOneBy).toHaveBeenCalledWith({ id: id })
|
||||
})
|
||||
|
||||
it('findAll => should find all track files by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const log = {
|
||||
id: '094ec69c-72c4-4995-8821-79d5b79bedda',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity;
|
||||
const tracks = [track]
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(log)
|
||||
jest.spyOn(mockTrackRepository, 'find').mockReturnValue(tracks);
|
||||
|
||||
const result = await service.findAll(logId);
|
||||
|
||||
expect(result).toEqual(tracks);
|
||||
expect(mockTrackRepository.find).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
log: {
|
||||
id: logId
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('findAll => should fail to find log', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(undefined);
|
||||
|
||||
try {
|
||||
await service.findAll(logId);
|
||||
|
||||
fail('find failed to throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CustomError);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(logId);
|
||||
}
|
||||
})
|
||||
|
||||
it('update => should update a track', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const track = {
|
||||
|
||||
} as TrackDto;
|
||||
const mockUpdateResult: UpdateResult = {
|
||||
affected: 1,
|
||||
raw: [],
|
||||
generatedMaps: []
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackRepository, 'update').mockReturnValue(mockUpdateResult)
|
||||
|
||||
const result = await service.update(id, track);
|
||||
|
||||
expect(result).toEqual(mockUpdateResult);
|
||||
expect(mockTrackRepository.update).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.update).toHaveBeenCalledWith(id, track)
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,11 @@ 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";
|
||||
import { LogService } from "../log/log.service";
|
||||
import { LogEntity } from "../log/log.entity";
|
||||
import { CustomError } from "../error/customError";
|
||||
import { FileService } from "../file/file.service";
|
||||
import { Logs } from "src/log/logs.interface";
|
||||
|
||||
@Injectable()
|
||||
export class TrackService {
|
||||
@@ -19,11 +20,7 @@ export class TrackService {
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<TrackEntity> {
|
||||
try {
|
||||
return await this.trackRepository.findOneBy({ id });
|
||||
} catch (error) {
|
||||
throw new CustomError('Track not found', 'Not found', 404)
|
||||
}
|
||||
return await this.trackRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
async findAll(logId: string): Promise<TrackEntity[]> {
|
||||
@@ -40,10 +37,11 @@ export class TrackService {
|
||||
})
|
||||
|
||||
return tracks;
|
||||
} else {
|
||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,21 +67,11 @@ export class TrackService {
|
||||
}
|
||||
|
||||
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
||||
try {
|
||||
return await this.trackRepository.update(id, track);
|
||||
} catch(error) {
|
||||
throw error
|
||||
}
|
||||
return await this.trackRepository.update(id, track);
|
||||
}
|
||||
|
||||
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
||||
try {
|
||||
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||
|
||||
return downloadedFile;
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
return await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||
}
|
||||
|
||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
||||
|
||||
9
api/src/transformers/columnNumeric.transformer.ts
Normal file
9
api/src/transformers/columnNumeric.transformer.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class ColumnNumericTransformer {
|
||||
to(data: number): number {
|
||||
return data;
|
||||
}
|
||||
|
||||
from (data: string): number {
|
||||
return parseFloat(data);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha",
|
||||
"version": "2.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -14,11 +14,10 @@
|
||||
"@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/typography": "^0.5.19",
|
||||
"@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",
|
||||
"oidc-spa": "^7.2.4",
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
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 { HeroUIProvider } from '@heroui/react';
|
||||
import { useHref, useNavigate } from 'react-router-dom';
|
||||
import './styles.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const App = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
||||
<div className='bg-[#f5f5f5]' data-theme="lofi">
|
||||
<SiteNav />
|
||||
<Routes>
|
||||
<Route path='/' element={<Flights />} />
|
||||
<Route path="/logbook" element={<Logbook />} />
|
||||
<Route path="/pilots" element={<Pilots />} />
|
||||
</Routes>
|
||||
</HeroUIProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +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`],
|
||||
issuerUri: import.meta.env.VITE_ISSUER_URI,
|
||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||
homeUrl: import.meta.env.VITE_BASE_URL,
|
||||
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
|
||||
autoLogin: false,
|
||||
postLoginRedirectUrl: '/',
|
||||
noIframe: true
|
||||
noIframe: true,
|
||||
}));
|
||||
@@ -1,93 +0,0 @@
|
||||
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 | HTMLElement>(
|
||||
// null
|
||||
// );
|
||||
// const auth = useAuth();
|
||||
|
||||
// const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
// setAnchorElAction(event.currentTarget);
|
||||
// };
|
||||
|
||||
// const onCloseActionMenu = () => {
|
||||
// setAnchorElAction(null);
|
||||
// };
|
||||
|
||||
const options = [
|
||||
'Item 1',
|
||||
'Item 2'
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
onOptionSelected={() => console.log('clicked!')}
|
||||
options={options}
|
||||
>
|
||||
<IconButton>
|
||||
<Icon className='text-2xl' iconName={IconName.ELLIPSIS_VERTICAL} />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</>
|
||||
// <div>
|
||||
// <IconButton onClick={onOpenActionMenu}>
|
||||
// <Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
||||
// </IconButton>
|
||||
// <Menu
|
||||
// anchorEl={anchorElAction}
|
||||
// keepMounted
|
||||
// open={Boolean(anchorElAction)}
|
||||
// onClose={onCloseActionMenu}
|
||||
// >
|
||||
// {auth.isAuthenticated &&
|
||||
// <>
|
||||
// <MenuItem onClick={() => onOpenCloseForm(FormMode.EDIT, id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.PEN} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>Edit</ListItemText>
|
||||
// </MenuItem>
|
||||
// {onOpenCloseTracks &&
|
||||
// <MenuItem onClick={() => onOpenCloseTracks!(FormMode.EDIT, id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.MAP_LOCATION_DOT} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>Tracks</ListItemText>
|
||||
// </MenuItem>
|
||||
// }
|
||||
// </>
|
||||
|
||||
// }
|
||||
// <MenuItem onClick={() => onOpenCloseForm(FormMode.VIEW, id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.EYE} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>View</ListItemText>
|
||||
// </MenuItem>
|
||||
// {auth.isAuthenticated &&
|
||||
// <>
|
||||
// <hr className="my-3" />
|
||||
// <MenuItem onClick={() => onDelete(id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.TRASH} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>Delete</ListItemText>
|
||||
// </MenuItem>
|
||||
// </>
|
||||
// }
|
||||
// </Menu>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionMenu;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
|
||||
export interface IActionMenuProps {
|
||||
id: string;
|
||||
onDelete: (entryId: string) => void;
|
||||
onOpenCloseForm: (formMode: FormMode, id: string) => void;
|
||||
onOpenCloseTracks?: (formMode: FormMode, id: string) => void;
|
||||
}
|
||||
44
client/src/components/alert/Alert.tsx
Normal file
44
client/src/components/alert/Alert.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AlertProps } from "./AlertProps.interface";
|
||||
import { faCircleCheck, faCircleInfo, faCircleXmark, faTriangleExclamation, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const Alert = ({
|
||||
children,
|
||||
className,
|
||||
closeIcon,
|
||||
severity,
|
||||
onClose,
|
||||
...rest
|
||||
}: AlertProps) => {
|
||||
const severityVariants = {
|
||||
info: 'alert-info',
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning'
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
role='alert'
|
||||
className={`alert ${severity ? severityVariants[severity] : ''} ${className ? className : ''}`}
|
||||
{...rest}
|
||||
>
|
||||
<span>
|
||||
{severity === 'info' && <FontAwesomeIcon icon={faCircleInfo} />}
|
||||
{severity === 'error' && <FontAwesomeIcon icon={faCircleXmark} />}
|
||||
{severity === 'success' && (
|
||||
<FontAwesomeIcon icon={faCircleCheck} />
|
||||
)}
|
||||
{severity === 'warning' && (
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} />
|
||||
)}
|
||||
</span>
|
||||
<span>{children}</span>
|
||||
{closeIcon && <button className='btn' onClick={onClose}>{<FontAwesomeIcon icon={faXmark} />}</button>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
7
client/src/components/alert/AlertProps.interface.ts
Normal file
7
client/src/components/alert/AlertProps.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface AlertProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
closeIcon?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
severity: 'info' | 'error' | 'success' | 'warning';
|
||||
}
|
||||
@@ -1,13 +1,3 @@
|
||||
// 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';
|
||||
@@ -21,31 +11,25 @@ const ConfirmationDialog = ({
|
||||
title
|
||||
}: DialogConfirmationProps) => {
|
||||
return (
|
||||
<Modal
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={true}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
<ModalBody>
|
||||
{!isLoading && <div>{contentText}</div>}
|
||||
{isLoading && <Spinner size='lg' />}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onCancel} startContent={<FontAwesomeIcon icon={faXmark} />}>
|
||||
No
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
onPress={onConfirm}
|
||||
startContent={<FontAwesomeIcon icon={faCircleCheck} />}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<>
|
||||
<input type='checkbox' id='dialog' className='modal-toggle' onChange={() => {}} checked={isOpen} />
|
||||
<div className='modal' role='dialog'>
|
||||
<div className='modal-box'>
|
||||
<h3 className="text-lg font-bold">{title}</h3>
|
||||
<p className="py-4">{contentText}</p>
|
||||
<div className='modal-action'>
|
||||
<button className='btn' onClick={onCancel}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
No
|
||||
</button>
|
||||
<button className='btn btn-primary' onClick={onConfirm}>
|
||||
<FontAwesomeIcon icon={faCircleCheck} />
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,76 +1,104 @@
|
||||
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'
|
||||
import { useOidc } from "../../auth/oidcConfig";
|
||||
import Alert from "../alert/Alert";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||
import { ScreenSize } from "../../enums/screenSize";
|
||||
|
||||
const Flights = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { logs, logsLoading } = useLogs();
|
||||
const { isUserLoggedIn } = useOidc();
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
useEffect(() => {
|
||||
const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => {
|
||||
if (log.tracks && log.tracks.length > 0) {
|
||||
return log;
|
||||
const getFlights = async (pageIndex: number, pageSize: number) => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/logs/flights`, {
|
||||
params: {
|
||||
skip: pageIndex,
|
||||
take: pageSize
|
||||
}
|
||||
});
|
||||
const flights: LogbookEntry[] = response.data.entities;
|
||||
const hasMore: boolean = response.data.hasNextPage;
|
||||
const total: number = response.data.total;
|
||||
|
||||
if (flights.length > 0) {
|
||||
const newFlights: LogbookEntry[] = [...state.flights, ...flights]
|
||||
const newPageIndex: number = state.pageIndex + flights.length;
|
||||
|
||||
dispatch({ type: 'SET_FLIGHTS', payload: { flights: newFlights, hasMoreFlights: hasMore, pageIndex: newPageIndex, totalFlights: total }})
|
||||
|
||||
if (!isUserLoggedIn && flights.length >= 5) {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'A limited number of flights displayed. Sign in to view all flights.'}})
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }})
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
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' }})
|
||||
dispatch({
|
||||
type: 'SET_ALERT',
|
||||
payload: { severity: 'error', message: `Loading of flights failed with the following message: ${axiosError.message}`}
|
||||
});
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
||||
}
|
||||
}, [logs])
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
getFlights(state.pageIndex, state.pageSize)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(logsLoading)
|
||||
}, [logsLoading])
|
||||
getFlights(state.pageIndex, state.pageSize);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='max-w-screen-lg mx-auto'>
|
||||
<div className={`max-w-screen-lg mx-auto ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD? 'mr-4 ml-4' : ''}`}>
|
||||
<div className='prose mt-5 mb-5'>
|
||||
<h1>Flights</h1>
|
||||
</div>
|
||||
{!logsLoading && state.alert && (
|
||||
{!state.isLoading && state.alert && (
|
||||
<div>
|
||||
<Alert
|
||||
className='mb-5'
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
color={state.alert.severity}
|
||||
title={state.alert.message}
|
||||
/>
|
||||
severity={state.alert.severity}
|
||||
>
|
||||
{state.alert.message}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{!logsLoading &&
|
||||
{!state.isLoading &&
|
||||
<div>
|
||||
<LogbookCard logs={state.flights} mode='flights' />
|
||||
<div>
|
||||
<LogbookCard logs={state.flights} mode='flights' />
|
||||
</div>
|
||||
{state.hasMoreFlights &&
|
||||
<div className='flex items-center justify-center mb-5'>
|
||||
<button className='btn btn-link' onClick={loadMore}>Load more</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{logsLoading && [...Array(6)].map((_element, index) => {
|
||||
{state.isLoading && [...Array(5)].map((_element, index) => {
|
||||
return (
|
||||
<div className='mb-5'>
|
||||
<Card
|
||||
key={index}
|
||||
>
|
||||
<div className='p-5'>
|
||||
<div className='mb-2'><Skeleton height='h-[60px]' width='w-[200px]' /></div>
|
||||
<div className='mb-2'><Skeleton height='h-[30px]' width='w-[200px]' /></div>
|
||||
<div className='mb-2'><Skeleton height='h-[300px]' width='w-[300px]' /></div>
|
||||
{[...Array(4)].map((_element, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||
<div className='card-body' key={index}>
|
||||
<div className='skeleton h-10 w-[150px]' />
|
||||
<div className='skeleton h-[350px]' />
|
||||
<div className='skeleton h-[65px]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -5,4 +5,8 @@ export interface FlightsState {
|
||||
alert: Alert | undefined;
|
||||
flights: LogbookEntry[];
|
||||
isLoading: boolean;
|
||||
hasMoreFlights: boolean;
|
||||
pageIndex: number;
|
||||
pageSize: number
|
||||
totalFlights: number;
|
||||
}
|
||||
@@ -5,13 +5,17 @@ import { FlightsState } from "./FlightsState.interface";
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_FLIGHTS'; payload: LogbookEntry[] }
|
||||
| { type: 'SET_FLIGHTS'; payload: { flights: LogbookEntry[], hasMoreFlights: boolean, pageIndex: number, totalFlights: number } }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
|
||||
export const initialState: FlightsState = {
|
||||
alert: undefined,
|
||||
flights: [],
|
||||
isLoading: true
|
||||
isLoading: true,
|
||||
hasMoreFlights: true,
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
totalFlights: 0
|
||||
}
|
||||
|
||||
export const reducer = (
|
||||
@@ -28,7 +32,10 @@ export const reducer = (
|
||||
case 'SET_FLIGHTS': {
|
||||
return {
|
||||
...state,
|
||||
flights: action.payload
|
||||
flights: action.payload.flights,
|
||||
hasMoreFlights: action.payload.hasMoreFlights,
|
||||
pageIndex: action.payload.pageIndex,
|
||||
totalFlights: action.payload.totalFlights
|
||||
}
|
||||
}
|
||||
case 'SET_IS_LOADING': {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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 { useEffect, useReducer } from 'react';
|
||||
import { useForm, Controller, useFormContext } from 'react-hook-form';
|
||||
import { LogFormProps } from './LogFormProps.interface';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
@@ -10,7 +9,6 @@ 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 = () => {
|
||||
@@ -40,7 +38,7 @@ const LogForm = () => {
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'danger', message: axiosError.message }});
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||
}
|
||||
@@ -55,11 +53,16 @@ const LogForm = () => {
|
||||
if (pilots && FormMode.ADD) {
|
||||
const newPilotsOptions = pilots.map((pilot) => {
|
||||
return {
|
||||
key: pilot.id,
|
||||
label: pilot.name,
|
||||
value: pilot.id,
|
||||
};
|
||||
});
|
||||
console.log(newPilotsOptions)
|
||||
|
||||
newPilotsOptions.unshift({
|
||||
label: '',
|
||||
value: '',
|
||||
})
|
||||
|
||||
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
||||
}
|
||||
}, [pilots]);
|
||||
@@ -73,25 +76,21 @@ const LogForm = () => {
|
||||
<Controller
|
||||
name="pilotId"
|
||||
control={control}
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
<select
|
||||
className='select w-full'
|
||||
aria-labelledby='pilot'
|
||||
isDisabled={state.isDisabled}
|
||||
fullWidth={true}
|
||||
isRequired={true}
|
||||
onSelectionChange={(keys: SharedSelection) => {
|
||||
setValue('pilotId', keys.currentKey);
|
||||
}}
|
||||
selectedKeys={[value]}
|
||||
size='lg'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
value={[value]}
|
||||
>
|
||||
{state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => {
|
||||
{state.pilotOptions?.map((pilotOption: { label: string, value: string; }) => {
|
||||
return (
|
||||
<SelectItem key={pilotOption.key}>{pilotOption.label}</SelectItem>
|
||||
<option value={pilotOption.value}>{pilotOption.label}</option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</select>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -104,21 +103,14 @@ const LogForm = () => {
|
||||
name="date"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const parsedAbsoluteDate = value ? parseAbsolute(value, getLocalTimeZone()) : value
|
||||
|
||||
return(
|
||||
<DatePicker
|
||||
aria-labelledby='date'
|
||||
isDisabled={state.isDisabled}
|
||||
isRequired={true}
|
||||
onChange={(selectedDate) => {
|
||||
let date = selectedDate as CalendarDate;
|
||||
|
||||
setValue('date', date.toDate(getLocalTimeZone()).toISOString())
|
||||
}}
|
||||
size='lg'
|
||||
value={parsedAbsoluteDate ? new CalendarDate(parsedAbsoluteDate.year, parsedAbsoluteDate.month, parsedAbsoluteDate.day) : value}
|
||||
/>
|
||||
<input
|
||||
type='date'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
value={value ? value.split('T')[0] : ''}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
@@ -131,18 +123,11 @@ const LogForm = () => {
|
||||
name="aircraftMakeModel"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
aria-labelledby='aircraftMakeModel'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isRequired={true}
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
size='lg'
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -158,18 +143,11 @@ const LogForm = () => {
|
||||
name="aircraftIdentity"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
aria-labelledby='aircraftIdentity'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isRequired={true}
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
size='lg'
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -185,18 +163,11 @@ const LogForm = () => {
|
||||
name="routeFrom"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
aria-labelledby='routeFrom'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isRequired={true}
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
size='lg'
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -210,18 +181,11 @@ const LogForm = () => {
|
||||
name="routeTo"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
aria-labelledby='routeTo'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isRequired={true}
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
size='lg'
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -235,21 +199,11 @@ const LogForm = () => {
|
||||
name="durationOfFlight"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='durationOfFlight'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isRequired={true}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -265,21 +219,11 @@ const LogForm = () => {
|
||||
name="singleEngineLand"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='singleEngineLand'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isRequired={true}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -293,20 +237,11 @@ const LogForm = () => {
|
||||
name="simulatorAtd"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='simulaterAtd'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -323,22 +258,11 @@ const LogForm = () => {
|
||||
name="landingsDay"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='landingsDay'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -352,22 +276,11 @@ const LogForm = () => {
|
||||
name="landingsNight"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='landingsNight'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -386,24 +299,12 @@ const LogForm = () => {
|
||||
name="groundTrainingReceived"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='groundTrainingReceived'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
width='w-full'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -418,22 +319,11 @@ const LogForm = () => {
|
||||
name="flightTrainingReceived"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='flightTrainingReceived'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -447,22 +337,11 @@ const LogForm = () => {
|
||||
name="crossCountry"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='crossCountry'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -476,22 +355,11 @@ const LogForm = () => {
|
||||
name="night"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='night'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -505,22 +373,11 @@ const LogForm = () => {
|
||||
name="solo"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='solo'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -534,22 +391,11 @@ const LogForm = () => {
|
||||
name="pilotInCommand"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='pilotInCommand'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -566,22 +412,11 @@ const LogForm = () => {
|
||||
name="instrumentActual"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='instrumentActual'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -595,22 +430,11 @@ const LogForm = () => {
|
||||
name="instrumentSimulated"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='instrumentSimulated'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -626,22 +450,11 @@ const LogForm = () => {
|
||||
name="instrumentApproaches"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='instrumentApproaches'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -655,22 +468,11 @@ const LogForm = () => {
|
||||
name="instrumentHolds"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='instrumentHolds'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -684,23 +486,13 @@ const LogForm = () => {
|
||||
name="instrumentNavTrack"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<NumberInput
|
||||
aria-labelledby='instrumentNavTrack'
|
||||
isDisabled={state.isDisabled}
|
||||
color={
|
||||
formState.errors.address ? 'danger' : undefined
|
||||
}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
isWheelDisabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
type="number"
|
||||
<input
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
min={0}
|
||||
onChange={(event) => onChange(Number(event.target.value))}
|
||||
step={0.1}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -716,19 +508,12 @@ const LogForm = () => {
|
||||
name="notes"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Textarea
|
||||
aria-labelledby='notes'
|
||||
isDisabled={state.isDisabled}
|
||||
color={formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
formState.errors.address
|
||||
? formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
<textarea
|
||||
className='textarea w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
></textarea>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { Selection } from "@heroui/react";
|
||||
import { Alert } from "../../interfaces/Alert.interface";
|
||||
|
||||
export interface LogFormState {
|
||||
alert: Alert | undefined;
|
||||
experienceSelectedKeys: Selection;
|
||||
instrumentSelectedKeys: Selection;
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
landingsSelectedKeys: Selection;
|
||||
pilotOptions: { key: string; label: string; }[];
|
||||
pilotOptions: { label: string; value: string; }[];
|
||||
selectedPilotName: string;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { Alert } from '../../interfaces/Alert.interface';
|
||||
import { LogFormState } from './LogFormState.interface';
|
||||
import { Selection } from '@heroui/react';
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_EXPERIENCE_SELECTED_KEYS'; payload: Selection }
|
||||
| { type: 'SET_INSTRUMENT_SELECTED_KEYS'; payload: Selection }
|
||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_LANDINGS_SELECTED_KEYS'; payload: Selection }
|
||||
| { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] }
|
||||
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
||||
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
||||
|
||||
export const initialState: LogFormState = {
|
||||
alert: undefined,
|
||||
experienceSelectedKeys: new Set([]),
|
||||
instrumentSelectedKeys: new Set([]),
|
||||
isDisabled: false,
|
||||
isLoading: true,
|
||||
landingsSelectedKeys: new Set(['1']),
|
||||
pilotOptions: [],
|
||||
selectedPilotName: ''
|
||||
};
|
||||
@@ -34,36 +27,18 @@ export const reducer = (
|
||||
alert: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_EXPERIENCE_SELECTED_KEYS': {
|
||||
return {
|
||||
...state,
|
||||
experienceSelectedKeys: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_IS_DISABLED': {
|
||||
return {
|
||||
...state,
|
||||
isDisabled: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_INSTRUMENT_SELECTED_KEYS': {
|
||||
return {
|
||||
...state,
|
||||
instrumentSelectedKeys: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_IS_LOADING': {
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_LANDINGS_SELECTED_KEYS': {
|
||||
return {
|
||||
...state,
|
||||
landingsSelectedKeys: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_PILOT_OPTIONS': {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Key, useEffect, useReducer } from 'react';
|
||||
import LogForm from '../logForm/LogForm';
|
||||
import { useEffect, useReducer, useState } from 'react';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { authColumns, unauthColumns } from './columns';
|
||||
import ActionMenu from '../actionMenu/ActionMenu';
|
||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
import LogbookCard from '../logbookCard/LogbookCard';
|
||||
@@ -14,21 +11,42 @@ import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||
import { UserRole } from '../../enums/userRole';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { ScreenSize } from '../../enums/screenSize';
|
||||
import { Table, TableHeader, TableBody, TableColumn, Dropdown, DropdownTrigger, Button, DropdownSection, DropdownMenu, DropdownItem, Alert, TableRow, TableCell } from '@heroui/react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
|
||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table';
|
||||
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, HeaderContext, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
||||
import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
|
||||
import Alert from '../alert/Alert';
|
||||
import TrackMap from '../trackMap/TrackMap';
|
||||
|
||||
interface ActionsProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Logbook: React.FC<unknown> = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const logbookContext = useLogbookContext()
|
||||
const [columnVisibility, setColumnVisibility] = useState({});
|
||||
const logbookContext = useLogbookContext();
|
||||
const { isUserLoggedIn } = useOidc();
|
||||
const { userRole } = useUserRole();
|
||||
const { screenSize } = useBreakpoints();
|
||||
const Actions = ({ id }: ActionsProps) => {
|
||||
return (
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div tabIndex={0} role='button' className='btn btn-ghost p-0'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
||||
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-sm border border-base-300 !z-[100]">
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||
}
|
||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onDeleteLog(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||
const blah = info.table
|
||||
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
||||
let total: number = 0;
|
||||
|
||||
@@ -54,7 +72,7 @@ const Logbook: React.FC<unknown> = () => {
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const date = new Date(info.getValue() as string);
|
||||
const date = new Date((info.getValue() as string).replace('Z', ''));
|
||||
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
|
||||
return formattedDate;
|
||||
@@ -69,13 +87,17 @@ const Logbook: React.FC<unknown> = () => {
|
||||
id: 'route',
|
||||
header: 'Route of Flight',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'routeFrom',
|
||||
accessorKey: 'routeFrom',
|
||||
header: 'From'
|
||||
header: 'From',
|
||||
meta: {
|
||||
className: 'border-l border-base-300',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'routeTo',
|
||||
@@ -89,8 +111,9 @@ const Logbook: React.FC<unknown> = () => {
|
||||
accessorKey: 'durationOfFlight',
|
||||
header: 'Duration Of Flight',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||
@@ -99,65 +122,27 @@ const Logbook: React.FC<unknown> = () => {
|
||||
const notes: ColumnDef<LogbookEntry> = {
|
||||
id: 'notes',
|
||||
accessorKey: 'notes',
|
||||
header: 'Notes'
|
||||
header: 'Notes',
|
||||
meta: {
|
||||
className: 'border-l border-base-300',
|
||||
}
|
||||
}
|
||||
const actions: ColumnDef<LogbookEntry> = {
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
meta: {
|
||||
align: 'center',
|
||||
headerAlign: 'center'
|
||||
align: 'text-center',
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly variant='light' size='lg'>
|
||||
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownSection showDivider>
|
||||
<DropdownItem
|
||||
key='edit'
|
||||
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}
|
||||
startContent={<FontAwesomeIcon icon={faPen} />}
|
||||
>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='view'
|
||||
onPress={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}
|
||||
startContent={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key='Delete'
|
||||
onPress={() => onDeleteLog(info.row.original.id)}
|
||||
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Actions id={info.row.original.id} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
||||
pilotName,
|
||||
date,
|
||||
aircraftMakeModel,
|
||||
route,
|
||||
durationOfFlight,
|
||||
notes
|
||||
]
|
||||
|
||||
const authColumns: ColumnDef<LogbookEntry>[] = [
|
||||
const columns: ColumnDef<LogbookEntry>[] = [
|
||||
pilotName,
|
||||
date,
|
||||
aircraftMakeModel,
|
||||
@@ -173,8 +158,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
accessorKey: 'singleEngineLand',
|
||||
header: 'Single Engine Land',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||
@@ -184,7 +169,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
id: 'landings',
|
||||
header: 'Landings',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
@@ -193,8 +179,9 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Day',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -203,8 +190,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Night',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -213,7 +200,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
id: 'instrument',
|
||||
header: 'Instrument',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
@@ -222,8 +210,9 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Actual',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||
@@ -234,8 +223,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Simulated',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -246,8 +235,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Approaches',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -256,8 +245,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Holds',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -266,8 +255,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Nav/Track',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -276,7 +265,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
id: 'experienceTraining',
|
||||
header: 'Type of pilot experience or training',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
@@ -285,8 +275,9 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Ground Training Received',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
className: 'border-l border-base-300',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -297,8 +288,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Flight Training Received',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -309,8 +300,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Cross Country',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -321,8 +312,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Night',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -333,8 +324,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Solo',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -345,8 +336,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
header: 'Pilot In Command',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
@@ -355,31 +346,87 @@ const Logbook: React.FC<unknown> = () => {
|
||||
},
|
||||
notes,
|
||||
actions
|
||||
]
|
||||
];
|
||||
|
||||
const getLogbookEntries = async () => {
|
||||
const onPaginationChange = (updater: any) => {
|
||||
const newPaginationState: PaginationState = typeof updater === 'function' ? updater(state.pagination) : updater;
|
||||
|
||||
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: state.entries,
|
||||
columns: columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: onPaginationChange,
|
||||
rowCount: state.totalEntries,
|
||||
state: {
|
||||
columnVisibility: columnVisibility,
|
||||
pagination: state.pagination
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
firstPage,
|
||||
getCanNextPage,
|
||||
getCanPreviousPage,
|
||||
getPageCount,
|
||||
getState,
|
||||
lastPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageIndex
|
||||
} = table;
|
||||
|
||||
const getLogbookEntries = async (pageIndex: number, pageSize: number) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||
|
||||
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
||||
const entries: LogbookEntry[] = response.data;
|
||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
const response: AxiosResponse = await httpClient.get(`api/logs`, {
|
||||
params: {
|
||||
skip: pageIndex * pageSize,
|
||||
take: pageSize
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.length > 0) {
|
||||
dispatch({ type: 'SET_ENTRIES', payload: response.data });
|
||||
if (response.data.entities.length > 0) {
|
||||
const entries: LogbookEntry[] = response.data.entities;
|
||||
const entryColumns: string[] = Object.keys(entries[0]);
|
||||
const columnVisibility: {[key: string]: boolean} = {}
|
||||
|
||||
if (state.alert) {
|
||||
for (const column of table.getAllLeafColumns()) {
|
||||
const entryColumnExists = entryColumns.find((entryColumn) => entryColumn === column.id);
|
||||
|
||||
if (entryColumnExists) {
|
||||
columnVisibility[column.id] = true
|
||||
} else {
|
||||
columnVisibility[column.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
columnVisibility['pilotName'] = true
|
||||
columnVisibility['actions'] = true
|
||||
|
||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
setColumnVisibility(columnVisibility)
|
||||
dispatch({ type: 'SET_ENTRIES', payload: { entries: entries, totalEntries: response.data.total }});
|
||||
|
||||
if (!isUserLoggedIn && response.data.entities.length >= 5) {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'A limited number of log entries displayed. Sign in to view all log entries.'}})
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined})
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No logbook entries found.'}})
|
||||
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: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||
});
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||
@@ -431,13 +478,13 @@ const Logbook: React.FC<unknown> = () => {
|
||||
payload: false
|
||||
});
|
||||
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
||||
await getLogbookEntries();
|
||||
await getLogbookEntries(state.pagination?.pageIndex, state.pagination?.pageSize);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ALERT',
|
||||
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||
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 });
|
||||
@@ -451,181 +498,269 @@ const Logbook: React.FC<unknown> = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// let newColumns: ColumnDef<LogbookEntry>[];
|
||||
const onRowsPerPageChanged = (event: any) => {
|
||||
const newPaginationState: PaginationState = {
|
||||
pageIndex: 0,
|
||||
pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries
|
||||
};
|
||||
|
||||
// if (userRole === UserRole.WRITE) {
|
||||
// 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 })
|
||||
// }, [isUserLoggedIn])
|
||||
|
||||
const table = useReactTable({
|
||||
data: state.entries,
|
||||
columns: authColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel()
|
||||
});
|
||||
|
||||
const textAlignment = {
|
||||
center: 'text-center',
|
||||
left: 'text-start',
|
||||
right: 'text-end'
|
||||
};
|
||||
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!logbookContext.state.isDrawerOpen) {
|
||||
getLogbookEntries();
|
||||
getLogbookEntries(state.pagination.pageIndex, state.pagination.pageSize);
|
||||
}
|
||||
}, [logbookContext.state.isDrawerOpen]);
|
||||
}, [logbookContext.state.isDrawerOpen, state.pagination.pageIndex, state.pagination.pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
const pages: number[] = [];
|
||||
|
||||
if (state.pagination?.pageSize) {
|
||||
const totalPages = getPageCount();
|
||||
|
||||
for(let i = 0; i < totalPages; i++) {
|
||||
pages.push(i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_PAGES', payload: pages })
|
||||
}, [state.totalEntries, state.pagination?.pageSize])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state.alert)
|
||||
}, [state.alert])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||
<div className='prose max-w-none col-span-10 mt-5 mb-5'>
|
||||
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
||||
<div className='prose max-w-none col-span-6 mt-5 mb-5'>
|
||||
<h1>Logbook</h1>
|
||||
</div>
|
||||
<div className='col-span-2 justify-self-end self-center'>
|
||||
<div className='col-span-6 justify-self-end self-center'>
|
||||
{userRole === UserRole.WRITE &&
|
||||
<Button
|
||||
color='primary'
|
||||
onPress={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||
startContent={<FontAwesomeIcon icon={faAdd} />}
|
||||
data-testid="pilot-add-button"
|
||||
data-theme="lofi"
|
||||
<button className='btn btn-primary'
|
||||
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAdd} />
|
||||
Add Entry
|
||||
</Button>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
{!state.isLoading && state.alert && (
|
||||
<div className='col-span-12 mb-5'>
|
||||
<Alert
|
||||
className='mb-5'
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
color={'default'}
|
||||
title={state.alert.message}
|
||||
/>
|
||||
severity={state.alert.severity}
|
||||
>
|
||||
{state.alert.message}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{!state.isLoading && (
|
||||
<div className='col-span-12'>
|
||||
{state.entries.length > 0 && screenSize !== ScreenSize.SM && (
|
||||
<div className='p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto shadow-small rounded-large w-full'>
|
||||
<table className='min-w-full h-auto table-auto w-full'>
|
||||
<thead className='[&>tr]:first:rounded-lg'>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<th
|
||||
className={`${header.column.columnDef.meta?.align ? textAlignment[header.column.columnDef.meta?.align] : ''} group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||
colSpan={header.colSpan}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/* {header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
) : null} */}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD && (
|
||||
<div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg '>
|
||||
<div className='overflow-x-auto mb-5'>
|
||||
<div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'>
|
||||
<label className='select select-sm select-ghost'>
|
||||
<span className='label'>Rows per page</span>
|
||||
<select
|
||||
defaultValue={1}
|
||||
onChange={onRowsPerPageChanged}
|
||||
value={state.pagination?.pageSize}
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={state.totalEntries}>All</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
<table className='table min-w-full h-auto table-auto w-full'>
|
||||
<thead className='bg-base-200'>
|
||||
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, headerIndex) => {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td
|
||||
className={`${cell.column.columnDef.meta?.align ? textAlignment[cell.column.columnDef.meta?.align] : ''} py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:z-1 [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
|
||||
key={cell.id}
|
||||
>
|
||||
<th
|
||||
className={`${header.column.columnDef.meta?.className ? header.column.columnDef.meta?.className : ''} group/th px-3 h-10 align-middle whitespace-nowrap text-foreground-500 text-tiny font-semibold ${headerGroupIndex === 0 ? 'first:rounded-tl-lg last:rounded-tr-lg' : ''} ${headerGroupIndex === table.getHeaderGroups().length - 1 ? 'first:rounded-bl-lg last:rounded-br-lg' : ''} data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||
colSpan={header.colSpan}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/* {header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
) : null} */}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td
|
||||
className={`py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
|
||||
key={cell.id}
|
||||
>
|
||||
<div className={`${cell.column.columnDef.meta?.align ? cell.column.columnDef.meta?.align : ''}`}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</tbody>
|
||||
<thead className='[&>tr]:first:rounded-lg'>
|
||||
{table.getFooterGroups().map((footerGroup, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={footerGroup.id}>
|
||||
{footerGroup.headers.map((header) => {
|
||||
return (
|
||||
<td
|
||||
className='group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
|
||||
key={header.id}
|
||||
align={header.column.columnDef.meta?.align}
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</tbody>
|
||||
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||
{table.getFooterGroups().map((footerGroup, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={footerGroup.id}>
|
||||
{footerGroup.headers.map((header) => {
|
||||
return (
|
||||
<td
|
||||
className='roup/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
|
||||
key={header.id}
|
||||
>
|
||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.footer,
|
||||
header.getContext()
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</thead>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
{state.pagination.pageSize !== state.totalEntries &&
|
||||
<div className='col-span-12 justify-self-center self-center'>
|
||||
<div className='join'>
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={firstPage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAnglesLeft} />
|
||||
</button>
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={previousPage}
|
||||
disabled={!getCanPreviousPage()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleLeft} />
|
||||
</button>
|
||||
{state.pages.map((page, index) => {
|
||||
return (
|
||||
<button
|
||||
className={`join-item btn btn-sm ${getState().pagination.pageIndex === index ? 'btn-active' : ''}`}
|
||||
onClick={() => setPageIndex(index)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={() => nextPage()}
|
||||
disabled={!getCanNextPage()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleRight} />
|
||||
</button>
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={lastPage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAnglesRight} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} />
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{/* {state.isLoading && !state.alert && (
|
||||
<>
|
||||
<div>
|
||||
<Loading size='xl' />
|
||||
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<div className='col-span-12'>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
const date = new Date(row.original.date.replace('Z', ''));
|
||||
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||
|
||||
return (
|
||||
<div className='card bg-base-100 border border-base-300 mb-5'>
|
||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
|
||||
<div className={`grid grid-cols-12 gap-3`}>
|
||||
<>
|
||||
<div className='col-span-8'>
|
||||
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||
</div>
|
||||
<div className='col-span-4 justify-self-end self-center'>
|
||||
<Actions id={row.original.id} />
|
||||
</div>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<>
|
||||
{cell.column.columnDef.header !== 'Actions' && cell.column.columnDef.header !== 'Date' && cell.column.columnDef.header !== 'Pilot' &&
|
||||
<>
|
||||
<div className='col-span-8 font-bold'>
|
||||
<span>{cell.getContext().column.columnDef.header?.toString()}</span>
|
||||
</div>
|
||||
<div className='col-span-4'>
|
||||
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
{state.isLoading && !state.alert && (
|
||||
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
|
||||
<div className='col-span-12 justify-self-center'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-12 justify-self-center'>
|
||||
Loading...
|
||||
</div>
|
||||
</>
|
||||
)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{logbookContext.state.isDrawerOpen && (
|
||||
<LogbookDrawer
|
||||
@@ -642,14 +777,6 @@ const Logbook: React.FC<unknown> = () => {
|
||||
title="Confirm Delete"
|
||||
/>
|
||||
)}
|
||||
{/* {state.isTracksOpen &&
|
||||
<LogTracks
|
||||
isDrawerOpen={state.isTracksOpen}
|
||||
mode={state.tracksMode}
|
||||
onOpenClose={(mode) => onOpenCloseTracks(mode)}
|
||||
selectedLogId={logbookContext.state.selectedLogId}
|
||||
/>
|
||||
} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||
import { ColumnDef, PaginationState } from '@tanstack/react-table';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Alert } from '../../interfaces/Alert.interface';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
@@ -10,4 +10,7 @@ export interface LogbookState {
|
||||
isConfirmDialogLoading: boolean;
|
||||
isConfirmDialogOpen: boolean;
|
||||
isLoading: boolean;
|
||||
pages: number[];
|
||||
pagination: PaginationState;
|
||||
totalEntries: number;
|
||||
}
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
import {
|
||||
CellContext,
|
||||
ColumnDef,
|
||||
HeaderContext,
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
|
||||
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||
const blah = info.table
|
||||
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
||||
let total: number = 0;
|
||||
|
||||
if (values.length > 0) {
|
||||
total = values.reduce((accumulator, currentValue) => accumulator + currentValue, total)
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
const pilotName: ColumnDef<LogbookEntry> = {
|
||||
id: 'pilotName',
|
||||
accessorKey: 'pilot',
|
||||
header: 'Pilot',
|
||||
footer: 'PAGE TOTALS',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const pilot: any = info.getValue();
|
||||
|
||||
return pilot.name
|
||||
}
|
||||
}
|
||||
const date: ColumnDef<LogbookEntry> = {
|
||||
id: 'date',
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const date = new Date(info.getValue() as string);
|
||||
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
const aircraftMakeModel: ColumnDef<LogbookEntry> = {
|
||||
id: 'aircraftMakeModel',
|
||||
accessorKey: 'aircraftMakeModel',
|
||||
header: 'Aircraft Make & Model'
|
||||
}
|
||||
const route: ColumnDef<LogbookEntry> = {
|
||||
id: 'route',
|
||||
header: 'Route of Flight',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'routeFrom',
|
||||
accessorKey: 'routeFrom',
|
||||
header: 'From'
|
||||
},
|
||||
{
|
||||
id: 'routeTo',
|
||||
accessorKey: 'routeTo',
|
||||
header: 'To'
|
||||
}
|
||||
]
|
||||
}
|
||||
const durationOfFlight: ColumnDef<LogbookEntry> = {
|
||||
id: 'durationOfFlight',
|
||||
accessorKey: 'durationOfFlight',
|
||||
header: 'Duration Of Flight',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
||||
}
|
||||
const notes: ColumnDef<LogbookEntry> = {
|
||||
id: 'notes',
|
||||
accessorKey: 'notes',
|
||||
header: 'Notes'
|
||||
}
|
||||
|
||||
export const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
||||
pilotName,
|
||||
date,
|
||||
aircraftMakeModel,
|
||||
route,
|
||||
durationOfFlight,
|
||||
notes
|
||||
]
|
||||
|
||||
export const authColumns: ColumnDef<LogbookEntry>[] = [
|
||||
pilotName,
|
||||
date,
|
||||
aircraftMakeModel,
|
||||
{
|
||||
id: 'aircraftIdentity',
|
||||
accessorKey: 'aircraftIdentity',
|
||||
header: 'Aircraft Identity',
|
||||
},
|
||||
route,
|
||||
durationOfFlight,
|
||||
{
|
||||
id: 'singleEngineLand',
|
||||
accessorKey: 'singleEngineLand',
|
||||
header: 'Single Engine Land',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'landings',
|
||||
header: 'Landings',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'landingsDay',
|
||||
accessorKey: 'landingsDay',
|
||||
header: 'Day',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'landingsNight',
|
||||
accessorKey: 'landingsNight',
|
||||
header: 'Night',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'instrument',
|
||||
header: 'Instrument',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'instrumentActual',
|
||||
accessorKey: 'instrumentActual',
|
||||
header: 'Actual',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||
},
|
||||
{
|
||||
id: 'instrumentSimulated',
|
||||
accessorKey: 'instrumentSimulated',
|
||||
header: 'Simulated',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'instrumentApproaches',
|
||||
accessorKey: 'instrumentApproaches',
|
||||
header: 'Approaches',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'instrumentHolds',
|
||||
accessorKey: 'instrumentHolds',
|
||||
header: 'Holds',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'instrumentNavTrack',
|
||||
accessorKey: 'instrumentNavTrack',
|
||||
header: 'Nav/Track',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'experienceTraining',
|
||||
header: 'Type of pilot experience or training',
|
||||
meta: {
|
||||
headerAlign: 'center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'groundTrainingReceived',
|
||||
accessorKey: 'groundTrainingReceived',
|
||||
header: 'Ground Training Received',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'flightTrainingReceived',
|
||||
accessorKey: 'flightTrainingReceived',
|
||||
header: 'Flight Training Received',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'crossCountry',
|
||||
accessorKey: 'crossCountry',
|
||||
header: 'Cross Country',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'night',
|
||||
accessorKey: 'night',
|
||||
header: 'Night',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'solo',
|
||||
accessorKey: 'solo',
|
||||
header: 'Solo',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
},
|
||||
{
|
||||
id: 'pilotInCommand',
|
||||
accessorKey: 'pilotInCommand',
|
||||
header: 'Pilot In Command',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'right',
|
||||
headerAlign: 'right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
}
|
||||
]
|
||||
},
|
||||
notes
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||
import { ColumnDef, PaginationState } from '@tanstack/react-table';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Alert } from '../../interfaces/Alert.interface';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
@@ -7,11 +7,12 @@ import { LogbookState } from './LogbookState.interface';
|
||||
type Action =
|
||||
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
|
||||
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
|
||||
| { type: 'SET_ENTRIES'; payload: { entries: LogbookEntry[], totalEntries: number } }
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean };
|
||||
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_PAGES'; payload: number[] }
|
||||
| { type: 'SET_PAGINATION'; payload: PaginationState };
|
||||
|
||||
export const initialState: LogbookState = {
|
||||
alert: undefined,
|
||||
@@ -19,7 +20,13 @@ export const initialState: LogbookState = {
|
||||
entries: [],
|
||||
isConfirmDialogLoading: false,
|
||||
isConfirmDialogOpen: false,
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
pages: [],
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 10
|
||||
},
|
||||
totalEntries: 0
|
||||
};
|
||||
|
||||
export const reducer = (
|
||||
@@ -42,7 +49,8 @@ export const reducer = (
|
||||
case 'SET_ENTRIES': {
|
||||
return {
|
||||
...state,
|
||||
entries: action.payload
|
||||
entries: action.payload.entries,
|
||||
totalEntries: action.payload.totalEntries
|
||||
};
|
||||
}
|
||||
case 'SET_ALERT': {
|
||||
@@ -63,6 +71,18 @@ export const reducer = (
|
||||
isLoading: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_PAGES': {
|
||||
return {
|
||||
...state,
|
||||
pages: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_PAGINATION': {
|
||||
return {
|
||||
...state,
|
||||
pagination: action.payload
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Accordion, AccordionItem, Card, CardBody, CardHeader } from '@heroui/react'
|
||||
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
||||
import TrackMap from "../trackMap/TrackMap";
|
||||
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||
import { ScreenSize } from "../../enums/screenSize";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{logs.map((log) => {
|
||||
console.log(log)
|
||||
const date = new Date(log.date);
|
||||
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
|
||||
const date = new Date(log.date.replace('Z', ''));
|
||||
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className='p-4' key={log.id}>
|
||||
<CardHeader>
|
||||
<h2 className='font-bold text-2xl'>{formattedDate}</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-2' : ''}`} key={log.id}>
|
||||
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||
<div>
|
||||
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
||||
<div className='mb-5'>
|
||||
<TrackMap
|
||||
@@ -26,49 +27,50 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<Accordion variant='bordered'>
|
||||
<AccordionItem key='1' title='Details'>
|
||||
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Aircraft Make and Model</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.aircraftMakeModel}</span>
|
||||
</div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Route From</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.routeFrom}</span>
|
||||
</div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Route To</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.routeTo}</span>
|
||||
</div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Duration Of Flight</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.durationOfFlight}</span>
|
||||
</div>
|
||||
{log.notes &&
|
||||
<>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Notes</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.notes}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
|
||||
<input type='checkbox' />
|
||||
<div className='collapse-title font-semibold'>Details</div>
|
||||
<div className='collapse-content'>
|
||||
<div className={`grid grid-cols-12 gap-3 ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'mr-[5%] ml-[5%]' : 'mr-[30%] ml-[30%]'} mt-4 mb-4`}>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Aircraft Make and Model</span>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.aircraftMakeModel}</span>
|
||||
</div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Route From</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.routeFrom}</span>
|
||||
</div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Route To</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.routeTo}</span>
|
||||
</div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Duration Of Flight</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.durationOfFlight}</span>
|
||||
</div>
|
||||
{log.notes &&
|
||||
<>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Notes</span>
|
||||
</div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.notes}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Alert, Button, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Tab, Tabs } from "@heroui/react";
|
||||
import { useState } from "react";
|
||||
import { LogbookDrawerProps } from "./LogbookDrawerProps.interface";
|
||||
import LogForm from "../logForm/LogForm";
|
||||
import TracksForm from "../tracksForm/TracksForm";
|
||||
import Alert from '../alert/Alert';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faClock, faMapLocationDot, faSave, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
@@ -9,10 +10,11 @@ import { FormMode } from "../../enums/formMode";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { AxiosError } from "axios";
|
||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||
import { Key, useState } from "react";
|
||||
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||
import { ScreenSize } from "../../enums/screenSize";
|
||||
|
||||
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
const [activeTab, setActiveTab] = useState<Key>('time');
|
||||
const [activeTab, setActiveTab] = useState<string>('time');
|
||||
const defaultValues = {
|
||||
pilotId: '',
|
||||
date: null,
|
||||
@@ -40,6 +42,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
};
|
||||
const methods = useForm();
|
||||
const logbookContext = useLogbookContext()
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
@@ -48,8 +51,8 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
};
|
||||
|
||||
const onSubmit = async (data: unknown) => {
|
||||
console.log(data)
|
||||
try {
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: true });
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true });
|
||||
|
||||
if (!logbookContext.state.selectedLogId) {
|
||||
@@ -59,116 +62,116 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
}
|
||||
|
||||
methods.reset(defaultValues);
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
||||
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
|
||||
|
||||
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }});
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }});
|
||||
} finally {
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false });
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
||||
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedKeyChanged = (key: React.Key) => {
|
||||
setActiveTab(key)
|
||||
const onTabClicked = (event: any) => {
|
||||
setActiveTab(event.target.ariaLabel)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
closeButton={
|
||||
<Button isIconOnly>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</Button>
|
||||
}
|
||||
isOpen={logbookContext.state.isDrawerOpen}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<DrawerContent>
|
||||
<FormProvider {...methods}>
|
||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<DrawerHeader>
|
||||
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
{logbookContext.state.formAlert && (
|
||||
<div className='col-span-12'>
|
||||
<div className='drawer drawer-end'>
|
||||
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={logbookContext.state.isDrawerOpen} />
|
||||
<div className="drawer-side">
|
||||
<label
|
||||
htmlFor='my-drawer-1'
|
||||
aria-label='close-sidebar'
|
||||
className='drawer-overlay'
|
||||
></label>
|
||||
<div className={`menu bg-base-100 text-base-content min-h-full p-4 ${screenSize === ScreenSize.SM ? 'w-full' : screenSize === ScreenSize.MD ? 'w-[66%]' : 'w-[25%]'}`}>
|
||||
<FormProvider {...methods}>
|
||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<div className='grid grid-cols-12 mb-6'>
|
||||
<div className="col-span-10">
|
||||
<h2 className="mt-0 mb-0 self-center">
|
||||
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-span-2 justify-self-end self-center">
|
||||
<button className="btn btn-ghost" onClick={onCancel}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{logbookContext.state.formAlert && (
|
||||
<Alert
|
||||
className='mb-5'
|
||||
onClose={() =>
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
|
||||
}
|
||||
color={logbookContext.state.formAlert.severity}
|
||||
title={logbookContext.state.formAlert.message}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Tabs
|
||||
color='default'
|
||||
fullWidth={true}
|
||||
onSelectionChange={onSelectedKeyChanged}
|
||||
selectedKey={activeTab as string}
|
||||
variant='solid'
|
||||
>
|
||||
<Tab
|
||||
key='time'
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
<span>Time</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LogForm />
|
||||
</Tab>
|
||||
{logbookContext.state.formMode !== FormMode.ADD &&
|
||||
<Tab
|
||||
key='tracks'
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon icon={faMapLocationDot} />
|
||||
<span>Tracks</span>
|
||||
</div>
|
||||
}
|
||||
severity={logbookContext.state.formAlert.severity}
|
||||
>
|
||||
<TracksForm />
|
||||
</Tab>
|
||||
}
|
||||
</Tabs>
|
||||
</DrawerBody>
|
||||
{activeTab !== 'tracks' &&
|
||||
<DrawerFooter>
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className='col-span-12 justify-self-end self-center'>
|
||||
<Button
|
||||
disabled={
|
||||
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
|
||||
? logbookContext.state.isFormDisabled
|
||||
: false
|
||||
}
|
||||
startContent={<FontAwesomeIcon icon={faXmark} />}
|
||||
onPress={onCancel}
|
||||
>
|
||||
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
|
||||
<Button
|
||||
className='ml-[10px]'
|
||||
color='primary'
|
||||
disabled={logbookContext.state.isFormDisabled}
|
||||
startContent={<FontAwesomeIcon icon={faSave} />}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
{logbookContext.state.formAlert.message}
|
||||
</Alert>
|
||||
)}
|
||||
<div className='tabs tabs-lift mb-5'>
|
||||
<label className='tab'>
|
||||
<input aria-label='time' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'time' ? true : false} />
|
||||
<FontAwesomeIcon className='mr-1'icon={faClock} />
|
||||
Time
|
||||
</label>
|
||||
<div className='tab-content border-base-300 p-6'>
|
||||
<LogForm />
|
||||
</div>
|
||||
{logbookContext.state.formMode !== FormMode.ADD &&
|
||||
<>
|
||||
<label className='tab'>
|
||||
<input aria-label='tracks' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'tracks' ? true : false} />
|
||||
<FontAwesomeIcon className='mr-1' icon={faMapLocationDot} />
|
||||
Tracks
|
||||
</label>
|
||||
<div className='tab-content border-base-300 p-6'>
|
||||
<TracksForm />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</DrawerFooter>
|
||||
}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{activeTab !== 'tracks' &&
|
||||
<div>
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className='col-span-12 justify-self-end self-center'>
|
||||
<button
|
||||
className='btn'
|
||||
disabled={
|
||||
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
|
||||
? logbookContext.state.isFormDisabled
|
||||
: false
|
||||
}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||
</button>
|
||||
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
|
||||
<button
|
||||
className='btn btn-primary ml-2.5'
|
||||
disabled={logbookContext.state.isFormDisabled}
|
||||
type="submit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Card, CardActions, CardBody, CardHeader} from "@noahspan/noahspan-components"
|
||||
import { PilotCardProps } from "./PilotCardProps.interface"
|
||||
import ActionMenu from "../actionMenu/ActionMenu"
|
||||
// import { Card, CardActions, CardBody, CardHeader} from "@noahspan/noahspan-components"
|
||||
// import { PilotCardProps } from "./PilotCardProps.interface"
|
||||
// import ActionMenu from "../actionMenu/ActionMenu"
|
||||
|
||||
const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
||||
return (
|
||||
<div>
|
||||
{pilots.map((pilot) => {
|
||||
return (
|
||||
<div>
|
||||
<Card key={pilot.id}>
|
||||
<CardBody>
|
||||
<CardHeader>{pilot.name}</CardHeader>
|
||||
<CardActions>
|
||||
<ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
||||
</CardActions>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
||||
// return (
|
||||
// <div>
|
||||
// {pilots.map((pilot) => {
|
||||
// return (
|
||||
// <div>
|
||||
// <Card key={pilot.id}>
|
||||
// <CardBody>
|
||||
// <CardHeader>{pilot.name}</CardHeader>
|
||||
// <CardActions>
|
||||
// <ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
||||
// </CardActions>
|
||||
// </CardBody>
|
||||
// </Card>
|
||||
// </div>
|
||||
// )
|
||||
// })}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
export default PilotCard;
|
||||
// export default PilotCard;
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Key, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { IPilotFormProps } from './IPilotFormProps';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
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';
|
||||
// import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||
// import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||
// import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
import httpClient from '../../httpClient/httpClient';
|
||||
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input, Autocomplete, AutocompleteItem, SelectItem, Select } from '@heroui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { states } from './states';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { ScreenSize } from '../../enums/screenSize';
|
||||
|
||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
pilotId,
|
||||
@@ -44,39 +45,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
});
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
|
||||
const onPeoplePickerSearch = async (
|
||||
value: string
|
||||
) => {
|
||||
setIsPeoplePickerLoading(true);
|
||||
setPeoplePickerValue(value)
|
||||
|
||||
try {
|
||||
if (value !== '') {
|
||||
const searchString: string = value;
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/msgraph/search?search=${searchString}`
|
||||
);
|
||||
|
||||
setPeoplePickerResults(response.data);
|
||||
} else {
|
||||
setPeoplePickerResults([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsPeoplePickerLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPersonSelected = (userPrincipalName: string) => {
|
||||
const person: Person | undefined = peoplePickerResults.find((person) => person.userPrincipalName === userPrincipalName as string);
|
||||
|
||||
methods.setValue('name', person?.displayName!);
|
||||
methods.setValue('userId', person?.userPrincipalName!);
|
||||
setPeoplePickerValue(person?.displayName!);
|
||||
setPeoplePickerResults([])
|
||||
};
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
@@ -138,245 +107,225 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}, [pilotId]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
closeButton={
|
||||
<Button isIconOnly>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</Button>
|
||||
}
|
||||
isOpen={isDrawerOpen}
|
||||
placement='right'
|
||||
data-testid="pilot-drawer"
|
||||
onClose={onCancel}
|
||||
size='xl'
|
||||
>
|
||||
<DrawerContent>
|
||||
<FormProvider {...methods}>
|
||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<DrawerHeader>
|
||||
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Name *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Autocomplete
|
||||
inputValue={peoplePickerValue}
|
||||
isLoading={isPeoplePickerLoading}
|
||||
items={peoplePickerResults}
|
||||
onInputChange={onPeoplePickerSearch}
|
||||
onSelectionChange={(key: Key | null) => onPersonSelected(key as string)}
|
||||
>
|
||||
{peoplePickerResults.map((person: Person) => (
|
||||
<AutocompleteItem key={person.userPrincipalName}>
|
||||
{person.displayName}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
{isUserLoggedIn &&
|
||||
<>
|
||||
<div className='col-span-3 self-center'>
|
||||
<span>Address *</span>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="address"
|
||||
control={methods.control}
|
||||
rules={{ required: 'An address is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
color={methods.formState.errors.address ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>City *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="city"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A city is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
color={methods.formState.errors.city ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
methods.formState.errors.city
|
||||
? methods.formState.errors.city.message
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>State *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="state"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A state must be selected' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
onChange={onChange}
|
||||
selectedKeys={[value]}
|
||||
>
|
||||
{states.map((state) => (
|
||||
<SelectItem key={state.value}>{state.label}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Postal Code *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="postalCode"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A postal code is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
color={methods.formState.errors.postalCode ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
methods.formState.errors.postalCode
|
||||
? methods.formState.errors.postalCode.message
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Email</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="email"
|
||||
control={methods.control}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address'
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
color={methods.formState.errors.email ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
methods.formState.errors.email
|
||||
? methods.formState.errors.email.message
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Phone Number</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="phone"
|
||||
control={methods.control}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
|
||||
message: 'Enter phone number as 123-456-7890'
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
color={methods.formState.errors.phone ? 'danger' : undefined}
|
||||
errorMessage={
|
||||
methods.formState.errors.phone
|
||||
? methods.formState.errors.phone.message
|
||||
: undefined
|
||||
}
|
||||
fullWidth={true}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{/* {isAuthenticated &&
|
||||
<Grid size={12}>
|
||||
<PilotFormMedical
|
||||
isDisabled={isDisabled}
|
||||
<div className='drawer drawer-end'>
|
||||
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={isDrawerOpen} />
|
||||
<div className='drawer-side'>
|
||||
<label
|
||||
htmlFor='my-drawer-1'
|
||||
aria-label='close-sidebar'
|
||||
className='drawer-overlay'
|
||||
></label>
|
||||
<div className={`menu bg-base-100 text-base-content min-h-full p-4 ${screenSize === ScreenSize.SM ? 'w-full' : screenSize === ScreenSize.MD ? 'w-[66%]' : 'w-[25%]'}`}>
|
||||
<FormProvider {...methods}>
|
||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className="col-span-10">
|
||||
<h2 className="mt-0 mb-0 self-center">
|
||||
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-span-2 justify-self-end self-center">
|
||||
<button className="btn btn-ghost" onClick={onCancel}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<span>Name *</span>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="name"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A name is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isUserLoggedIn &&
|
||||
<>
|
||||
<div className='col-span-3 self-center'>
|
||||
<span>Address *</span>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="address"
|
||||
control={methods.control}
|
||||
rules={{ required: 'An address is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>City *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="city"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A city is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>State *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="state"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A state must be selected' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<select
|
||||
className='select w-full'
|
||||
onChange={onChange}
|
||||
value={[value]}
|
||||
>
|
||||
{states.map((state) => (
|
||||
<option key={state.value}>{state.label}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Postal Code *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="postalCode"
|
||||
control={methods.control}
|
||||
rules={{ required: 'A postal code is required' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Email</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="email"
|
||||
control={methods.control}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address'
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<h6>Phone Number</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<Controller
|
||||
name="phone"
|
||||
control={methods.control}
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
|
||||
message: 'Enter phone number as 123-456-7890'
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type='text'
|
||||
className='input w-full'
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{/* {isAuthenticated &&
|
||||
<Grid size={12}>
|
||||
<PilotFormMedical
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Grid>
|
||||
}
|
||||
<Grid size={12}>
|
||||
<PilotFormCertificates isDisabled={isDisabled} mode={mode} />
|
||||
</Grid>
|
||||
}
|
||||
<Grid size={12}>
|
||||
<PilotFormCertificates isDisabled={isDisabled} mode={mode} />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
||||
</Grid> */}
|
||||
{/* <div className='col-span-12 justify-self-end self-center'> */}
|
||||
<Grid size={12}>
|
||||
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
||||
</Grid> */}
|
||||
{/* <div className='col-span-12 justify-self-end self-center'> */}
|
||||
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
disabled={
|
||||
isDisabled && mode.toString() !== FormMode.VIEW
|
||||
? isDisabled
|
||||
: false
|
||||
}
|
||||
startContent={<FontAwesomeIcon icon={faXmark} />}
|
||||
onPress={onCancel}
|
||||
data-testid="pilot-cancel-button"
|
||||
>
|
||||
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
{mode.toString() !== FormMode.VIEW && (
|
||||
<Button
|
||||
color='primary'
|
||||
disabled={isDisabled}
|
||||
startContent={<FontAwesomeIcon icon={faSave} />}
|
||||
type="submit"
|
||||
data-testid="pilot-save-button"
|
||||
|
||||
<div className='col-span-12 justify-self-end self-center'>
|
||||
<button
|
||||
className='btn'
|
||||
disabled={
|
||||
isDisabled && mode.toString() !== FormMode.VIEW
|
||||
? isDisabled
|
||||
: false
|
||||
}
|
||||
onClick={onCancel}
|
||||
data-testid="pilot-cancel-button"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</DrawerFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||
</button>
|
||||
{mode.toString() !== FormMode.VIEW && (
|
||||
<button
|
||||
className='btn btn-primary ml-2.5'
|
||||
disabled={isDisabled}
|
||||
type="submit"
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,153 +1,153 @@
|
||||
import { PilotFormCertificatesProps } from './PilotFormCertificatesProps.interface';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
Input,
|
||||
Select
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
// import { PilotFormCertificatesProps } from './PilotFormCertificatesProps.interface';
|
||||
// import {
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Icon,
|
||||
// IconButton,
|
||||
// IconName,
|
||||
// Input,
|
||||
// Select
|
||||
// } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
// import { FormMode } from '../../enums/formMode';
|
||||
|
||||
const PilotFormCertificates = ({
|
||||
isDisabled,
|
||||
mode
|
||||
}: PilotFormCertificatesProps ) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
// const PilotFormCertificates = ({
|
||||
// isDisabled,
|
||||
// mode
|
||||
// }: PilotFormCertificatesProps ) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// } = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'certificates',
|
||||
control
|
||||
});
|
||||
// const { fields, append, remove } = useFieldArray({
|
||||
// name: 'certificates',
|
||||
// control
|
||||
// });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
<div>
|
||||
<h5>Certificates</h5>
|
||||
</div>
|
||||
}
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h6>Type</h6>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Number</h6>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Date of Issue</h6>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Controller
|
||||
name={`certificates.${index}.type`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: 'Student',
|
||||
value: 'student'
|
||||
},
|
||||
{
|
||||
label: 'Private',
|
||||
value: 'private'
|
||||
},
|
||||
{
|
||||
label: 'Instrument',
|
||||
value: 'instrument'
|
||||
},
|
||||
{
|
||||
label: 'Recreational',
|
||||
value: 'recreational'
|
||||
},
|
||||
{
|
||||
label: 'Sport',
|
||||
value: 'sport'
|
||||
}
|
||||
]
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name={`certificates.${index}.number`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name={`certificates.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
disabled={isDisabled}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Icon iconName={IconName.TRASH} size='sm' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!isDisabled &&
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
number: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
startContent={<Icon iconName={IconName.PLUS} />}
|
||||
>
|
||||
Add Certificate
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <div>
|
||||
// {fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
// <div>
|
||||
// <h5>Certificates</h5>
|
||||
// </div>
|
||||
// }
|
||||
// {fields.length > 0 && (
|
||||
// <>
|
||||
// <div>
|
||||
// <h6>Type</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Number</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Date of Issue</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// </div>
|
||||
// {fields.map((field, index) => {
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`certificates.${index}.type`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Select
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// options={
|
||||
// [
|
||||
// {
|
||||
// label: 'Student',
|
||||
// value: 'student'
|
||||
// },
|
||||
// {
|
||||
// label: 'Private',
|
||||
// value: 'private'
|
||||
// },
|
||||
// {
|
||||
// label: 'Instrument',
|
||||
// value: 'instrument'
|
||||
// },
|
||||
// {
|
||||
// label: 'Recreational',
|
||||
// value: 'recreational'
|
||||
// },
|
||||
// {
|
||||
// label: 'Sport',
|
||||
// value: 'sport'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`certificates.${index}.number`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Input
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// )
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`certificates.${index}.dateOfIssue`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <IconButton
|
||||
// disabled={isDisabled}
|
||||
// onClick={() => remove(index)}
|
||||
// >
|
||||
// <Icon iconName={IconName.TRASH} size='sm' />
|
||||
// </IconButton>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// })}
|
||||
// </>
|
||||
// )}
|
||||
// {!isDisabled &&
|
||||
// <div>
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// append({
|
||||
// type: '',
|
||||
// number: '',
|
||||
// dateOfIssue: null
|
||||
// });
|
||||
// }}
|
||||
// startContent={<Icon iconName={IconName.PLUS} />}
|
||||
// >
|
||||
// Add Certificate
|
||||
// </Button>
|
||||
// </div>
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default PilotFormCertificates;
|
||||
// export default PilotFormCertificates;
|
||||
|
||||
@@ -1,129 +1,129 @@
|
||||
import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interface';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
Select
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
// import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interface';
|
||||
// import {
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Icon,
|
||||
// IconButton,
|
||||
// IconName,
|
||||
// Select
|
||||
// } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
// import { FormMode } from '../../enums/formMode';
|
||||
|
||||
const PilotFormEndorsements = ({
|
||||
mode,
|
||||
isDisabled
|
||||
}: PilotFormEndorsementsProps) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
// const PilotFormEndorsements = ({
|
||||
// mode,
|
||||
// isDisabled
|
||||
// }: PilotFormEndorsementsProps) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// setValue
|
||||
// } = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'endorsements',
|
||||
control
|
||||
});
|
||||
// const { fields, append, remove } = useFieldArray({
|
||||
// name: 'endorsements',
|
||||
// control
|
||||
// });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
<div>
|
||||
<h5>Endorsements</h5>
|
||||
</div>
|
||||
}
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h6>Type</h6>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Date of Issue</h6>
|
||||
</div>
|
||||
<div></div>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Controller
|
||||
name={`endorsements.${index}.type`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: 'Complex',
|
||||
value: 'complex'
|
||||
},
|
||||
{
|
||||
label: 'High Performance',
|
||||
value: 'highPerfomance'
|
||||
},
|
||||
{
|
||||
label: 'High Altitude',
|
||||
value: 'highAltitude'
|
||||
},
|
||||
{
|
||||
label: 'Tailwheel',
|
||||
value: 'tailwheel'
|
||||
}
|
||||
]
|
||||
}
|
||||
value={value ? value : ''}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name={`endorsements.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
disabled={isDisabled}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Icon iconName={IconName.TRASH} size="sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!isDisabled &&
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
startContent={<Icon iconName={IconName.PLUS} />}
|
||||
>
|
||||
Add Endorsement
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <div>
|
||||
// {fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
// <div>
|
||||
// <h5>Endorsements</h5>
|
||||
// </div>
|
||||
// }
|
||||
// {fields.length > 0 && (
|
||||
// <>
|
||||
// <div>
|
||||
// <h6>Type</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Date of Issue</h6>
|
||||
// </div>
|
||||
// <div></div>
|
||||
// {fields.map((field, index) => {
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`endorsements.${index}.type`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Select
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// options={
|
||||
// [
|
||||
// {
|
||||
// label: 'Complex',
|
||||
// value: 'complex'
|
||||
// },
|
||||
// {
|
||||
// label: 'High Performance',
|
||||
// value: 'highPerfomance'
|
||||
// },
|
||||
// {
|
||||
// label: 'High Altitude',
|
||||
// value: 'highAltitude'
|
||||
// },
|
||||
// {
|
||||
// label: 'Tailwheel',
|
||||
// value: 'tailwheel'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// value={value ? value : ''}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`endorsements.${index}.dateOfIssue`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <IconButton
|
||||
// disabled={isDisabled}
|
||||
// onClick={() => remove(index)}
|
||||
// >
|
||||
// <Icon iconName={IconName.TRASH} size="sm" />
|
||||
// </IconButton>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// })}
|
||||
// </>
|
||||
// )}
|
||||
// {!isDisabled &&
|
||||
// <div>
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// append({
|
||||
// type: '',
|
||||
// dateOfIssue: null
|
||||
// });
|
||||
// }}
|
||||
// startContent={<Icon iconName={IconName.PLUS} />}
|
||||
// >
|
||||
// Add Endorsement
|
||||
// </Button>
|
||||
// </div>
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default PilotFormEndorsements;
|
||||
// export default PilotFormEndorsements;
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { DatePicker, Select } from '@noahspan/noahspan-components';
|
||||
import { Controller, useFormContext } from "react-hook-form"
|
||||
import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
||||
// import { DatePicker, Select } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFormContext } from "react-hook-form"
|
||||
// import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
||||
|
||||
const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
// const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// setValue
|
||||
// } = useFormContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h5>Medical</h5>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Class</h6>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="medicalClass"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: 'First',
|
||||
value: 'first'
|
||||
},
|
||||
{
|
||||
label: 'Second',
|
||||
value: 'second'
|
||||
},
|
||||
{
|
||||
label: 'Third',
|
||||
value: 'third'
|
||||
},
|
||||
{
|
||||
label: 'Basic Med',
|
||||
value: 'basicMed'
|
||||
}
|
||||
]
|
||||
}
|
||||
value={value ? value : ''}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Expiration</h6>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="medicalExpiration"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// return (
|
||||
// <div>
|
||||
// <div>
|
||||
// <h5>Medical</h5>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Class</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="medicalClass"
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Select
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// options={
|
||||
// [
|
||||
// {
|
||||
// label: 'First',
|
||||
// value: 'first'
|
||||
// },
|
||||
// {
|
||||
// label: 'Second',
|
||||
// value: 'second'
|
||||
// },
|
||||
// {
|
||||
// label: 'Third',
|
||||
// value: 'third'
|
||||
// },
|
||||
// {
|
||||
// label: 'Basic Med',
|
||||
// value: 'basicMed'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// value={value ? value : ''}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Expiration</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="medicalExpiration"
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
export default PilotFormMedical
|
||||
// export default PilotFormMedical
|
||||
@@ -4,20 +4,44 @@ import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||
import PilotCard from '../pilotCard/PilotCard';
|
||||
// import PilotCard from '../pilotCard/PilotCard';
|
||||
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||
import { UserRole } from '../../enums/userRole';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
import { ScreenSize } from '../../enums/screenSize';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEllipsisVertical, faPen, faEye, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisVertical, faPen, faEye, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { Pilot } from './Pilot.interface';
|
||||
import Alert from '../alert/Alert';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
|
||||
interface ActionsProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Pilots: React.FC<unknown> = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { userRole } = useUserRole();
|
||||
const { screenSize } = useBreakpoints()
|
||||
const { screenSize } = useBreakpoints();
|
||||
const { isUserLoggedIn } = useOidc();
|
||||
const Actions = ({ id }: ActionsProps) => {
|
||||
return (
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
||||
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300">
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||
}
|
||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onDeletePilot(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getPilots = async () => {
|
||||
try {
|
||||
@@ -34,7 +58,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }})
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No pilots found.' }})
|
||||
dispatch({ type: 'SET_PILOTS', payload: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -42,7 +66,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ALERT',
|
||||
payload: { severity: 'danger', message: `Loading of pilots failed with the following message: ${axiosError.message}`}
|
||||
payload: { severity: 'error', message: `Loading of pilots failed with the following message: ${axiosError.message}`}
|
||||
})
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
||||
@@ -50,7 +74,6 @@ const Pilots: React.FC<unknown> = () => {
|
||||
};
|
||||
|
||||
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
|
||||
console.log(pilotId)
|
||||
switch (mode) {
|
||||
case FormMode.ADD:
|
||||
case FormMode.EDIT:
|
||||
@@ -102,7 +125,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ALERT',
|
||||
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||
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 });
|
||||
@@ -116,64 +139,39 @@ const Pilots: React.FC<unknown> = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
const columns: ColumnDef<Pilot>[]= [
|
||||
{
|
||||
id: 'name',
|
||||
name: 'Name'
|
||||
accessorKey: 'name',
|
||||
header: 'Name'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
name: 'Actions'
|
||||
header: 'Actions',
|
||||
meta: {
|
||||
align: 'text-center',
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
cell: (info: CellContext<Pilot, unknown>) => {
|
||||
return (
|
||||
<Actions id={info.row.original.id} />
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const renderCell = (pilot: any, columnKey: any) => {
|
||||
const cellValue = pilot[columnKey]
|
||||
const textAlignment = {
|
||||
center: 'text-center',
|
||||
left: 'text-start',
|
||||
right: 'text-end'
|
||||
};
|
||||
|
||||
switch (columnKey) {
|
||||
case 'actions': {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly variant='light' size='lg'>
|
||||
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownSection showDivider>
|
||||
<DropdownItem
|
||||
key='edit'
|
||||
onPress={() => onOpenClosePilotForm(FormMode.EDIT, pilot.id)}
|
||||
startContent={<FontAwesomeIcon icon={faPen} />}
|
||||
>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='view'
|
||||
onPress={() => onOpenClosePilotForm(FormMode.VIEW, pilot.id)}
|
||||
startContent={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key='Delete'
|
||||
onPress={() => onDeletePilot(pilot.id)}
|
||||
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return cellValue
|
||||
}
|
||||
}
|
||||
}
|
||||
const table = useReactTable({
|
||||
data: state.pilots,
|
||||
columns: columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel()
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.isFormOpen) {
|
||||
@@ -183,63 +181,127 @@ const Pilots: React.FC<unknown> = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||
<div className='prose max-w-none col-span-10 mt-5 mb-5' >
|
||||
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
||||
<div className='prose max-w-none col-span-6 mt-5 mb-5' >
|
||||
<h1>Pilots</h1>
|
||||
</div>
|
||||
<div className='col-span-2 justify-self-end self-center'>
|
||||
{userRole === UserRole.WRITE &&
|
||||
<Button
|
||||
color='primary'
|
||||
onPress={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||
startContent={<FontAwesomeIcon icon={faPlus} />}
|
||||
<div className='col-span-6 justify-self-end self-center'>
|
||||
{!state.isLoading && userRole === UserRole.WRITE &&
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||
data-testid="pilot-add-button"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
Add Pilot
|
||||
</Button>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
{!state.isLoading && state.alert && (
|
||||
<div className='col-span-12'>
|
||||
<Alert
|
||||
className='mb-5'
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
color={state.alert.severity}
|
||||
title={state.alert.message}
|
||||
/>
|
||||
severity={state.alert.severity}
|
||||
>
|
||||
{state.alert.message}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className='col-span-12'>
|
||||
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
|
||||
<Table>
|
||||
<TableHeader columns={columns}>
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.id}
|
||||
align={column.id === "actions" ? "center" : "start"}
|
||||
>
|
||||
{column.name}
|
||||
</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody items={state.pilots}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey => (
|
||||
<TableCell>
|
||||
{renderCell(item, columnKey)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
|
||||
}
|
||||
</div>
|
||||
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
|
||||
<div className='col-span-12 bg-base-100 p-5 border border-base-100 rounded-lg'>
|
||||
<table className='table min-w-full h-auto table-auto w-full'>
|
||||
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<th
|
||||
className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''} group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold rounded data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||
colSpan={header.colSpan}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/* {header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
) : null} */}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td
|
||||
className={`${cell.column.columnDef.meta?.align ? cell.column.columnDef.meta?.align : ''} py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:z-1 [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
|
||||
key={cell.id}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<div className='col-span-12'>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<div className='card bg-base-100 border border-base-300 mb-5'>
|
||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
|
||||
<div className={`grid grid-cols-12 gap-3`}>
|
||||
<>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<>
|
||||
{cell.column.columnDef.header !== 'Actions' &&
|
||||
<>
|
||||
<div className='col-span-10 self-center'>
|
||||
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<Actions id={row.original.id} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{state.isFormOpen && (
|
||||
<PilotForm
|
||||
|
||||
@@ -3,17 +3,18 @@ import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
import { Avatar, Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, DropdownTrigger, DropdownMenu, DropdownItem, Dropdown } from '@heroui/react';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { faBars, faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { ScreenSize } from '../../enums/screenSize';
|
||||
|
||||
const SiteNav = () => {
|
||||
const [userPhoto, setUserPhoto] = useState<string>();
|
||||
const appContext = useAppContext();
|
||||
const { screenSize } = useBreakpoints();
|
||||
const { isUserLoggedIn, logout, login } = useOidc()
|
||||
const { pathname } = useLocation()
|
||||
const pages = [
|
||||
{
|
||||
name: 'Flights',
|
||||
@@ -53,13 +54,39 @@ const SiteNav = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const Brand = () => {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
className='mr-1'
|
||||
height={35}
|
||||
width={35}
|
||||
src='noahspan-logo.png'
|
||||
/>
|
||||
<FontAwesomeIcon className='mt-1' icon={faPlane} size='2x' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Links = () => {
|
||||
return (
|
||||
<>
|
||||
{pages.map((page) => {
|
||||
return (
|
||||
<li><NavLink to={page.path}>{page.name}</NavLink></li>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const setUserProfile = async () => {
|
||||
try {
|
||||
const userProfile = await getUserProfile();
|
||||
const userPhoto = await getUserPhoto();
|
||||
// const userPhoto = await getUserPhoto();
|
||||
|
||||
setUserPhoto(userPhoto);
|
||||
// setUserPhoto(userPhoto);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
@@ -79,53 +106,62 @@ const SiteNav = () => {
|
||||
}, [isUserLoggedIn]);
|
||||
|
||||
return (
|
||||
<Navbar isBordered maxWidth='full' position='static'>
|
||||
<NavbarContent>
|
||||
<NavbarBrand>
|
||||
<img
|
||||
height={35}
|
||||
width={35}
|
||||
src='noahspan-logo.png'
|
||||
style={{ marginRight: '5px' }}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faPlane} size='2x' />
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify='center'>
|
||||
{pages.length > 0 && pages.map((page, index) => {
|
||||
return (
|
||||
<NavbarItem isActive={pathname === page.path ? true : false} key={index}>
|
||||
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}>
|
||||
{page.name}
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
)
|
||||
})}
|
||||
</NavbarContent>
|
||||
<NavbarContent justify='end'>
|
||||
<div className="navbar bg-base-100 shadow-sm w-full">
|
||||
<div className={`navbar-start ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
|
||||
{screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
|
||||
<div className="dropdown">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
|
||||
<FontAwesomeIcon icon={faBars} size='xl' />
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={-1}
|
||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||
<Links />
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<Brand />
|
||||
)}
|
||||
</div>
|
||||
<div className="navbar-center">
|
||||
<ul className="menu menu-horizontal px-1">
|
||||
{screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
|
||||
<Brand />
|
||||
) : (
|
||||
<Links />
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={`navbar-end ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'mr-8' : ''}`}>
|
||||
{!isUserLoggedIn &&
|
||||
<Button
|
||||
color='default'
|
||||
onPress={() => login()}
|
||||
startContent={<FontAwesomeIcon icon={faSignIn} />}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<button className='btn btn-ghost' onClick={() => login()}><FontAwesomeIcon icon={faSignIn} />Sign In</button>
|
||||
}
|
||||
{isUserLoggedIn &&
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Avatar name={appContext.state.userProfile.displayName?.toString()} src={userPhoto}></Avatar>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem key='signout' onPress={() => logout({redirectTo: 'specific url', url: '/'})} startContent={<FontAwesomeIcon icon={faSignOut} />}>
|
||||
Sign Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div tabIndex={0} role='button'>
|
||||
<div className={`avatar ${userPhoto ? userPhoto : 'avatar-placeholder'}`}>
|
||||
{userPhoto &&
|
||||
<div className='w-12 rounded-full'>
|
||||
<img src={userPhoto} />
|
||||
</div>
|
||||
}
|
||||
{!userPhoto &&
|
||||
<div className='bg-neutral text-neutral-content w-10 rounded-full'>
|
||||
<span>NS</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300'
|
||||
>
|
||||
<li><a onClick={() => logout({redirectTo: 'specific url', url: '/'})}><FontAwesomeIcon icon={faSignOut} />Sign Out</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #eee;
|
||||
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.swiper-slide {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.swiper-slide img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet {
|
||||
background-color: #000000;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
border: 2px solid #FFFFFF;
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet-active {
|
||||
box-shadow: 0 0 0 1px #000000;
|
||||
}
|
||||
@@ -4,10 +4,6 @@ import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAuth } from 'react-oidc-context'
|
||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||
import ReactLeafletKml from 'react-leaflet-kml';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import 'swiper/css';
|
||||
import './TrackMap.css';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
|
||||
@@ -41,7 +37,7 @@ const TrackMap = ({ height, logId, tracks }: TrackMapProps) => {
|
||||
center={[45.14489, -93.21019]}
|
||||
scrollWheelZoom={false}
|
||||
style={{ height: height, width: '100%' }}
|
||||
zoom={8}
|
||||
zoom={7}
|
||||
>
|
||||
<Suspense>
|
||||
<TileLayer
|
||||
|
||||
@@ -7,7 +7,6 @@ import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
||||
import { initialState, reducer } from "./reducer";
|
||||
import TrackMap from "../trackMap/TrackMap";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { Button, Input, Spinner } from '@heroui/react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||
@@ -28,7 +27,7 @@ const TracksForm = () => {
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,23 +100,13 @@ const TracksForm = () => {
|
||||
}, [logbookContext.state.selectedLogId])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(logbookContext.state.formMode)
|
||||
if (logbookContext.state.formMode === FormMode.VIEW) {
|
||||
dispatch({ type: 'SET_IS_DISABLED', payload: true });
|
||||
}
|
||||
}, [logbookContext.state.formMode]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state.isDisabled)
|
||||
}, [state.isDisabled])
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
{state.tracks.length > 0 &&
|
||||
<div className="col-span-12">
|
||||
<TrackMap height='400px' logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
|
||||
</div>
|
||||
}
|
||||
<>
|
||||
{state.tracks.length > 0 && state.tracks.map((track, index) => {
|
||||
const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
|
||||
@@ -125,26 +114,25 @@ const TracksForm = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='col-span-10'>
|
||||
<Input isDisabled={state.isDisabled} key={index} type='text' value={filename}/>
|
||||
<input className='input w-full' disabled={state.isDisabled} key={index} readOnly type='text' value={filename} />
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<Button isDisabled={state.isDisabled} key={index} isIconOnly onPress={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
<button className='btn w-full' disabled={state.isDisabled} key={index} onClick={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
<div className='col-span-12'>
|
||||
<Button
|
||||
as='label'
|
||||
color='primary'
|
||||
isDisabled={state.isDisabled}
|
||||
fullWidth={true}
|
||||
startContent={<FontAwesomeIcon icon={faUpload} />}
|
||||
>
|
||||
Upload Track
|
||||
<input hidden onChange={handleFileUpload} type='file' />
|
||||
</Button>
|
||||
</div>
|
||||
{logbookContext.state.formMode === FormMode.EDIT &&
|
||||
<div className='col-span-12'>
|
||||
<label
|
||||
className='btn cursor-pointer w-full'
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} />
|
||||
Upload Track
|
||||
<input className='hidden' id='track-upload' onChange={handleFileUpload} type='file' />
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{state.isConfirmDialogOpen && (
|
||||
<ConfirmationDialog
|
||||
contentText="Are you sure you want to delete this track?"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export enum ScreenSize {
|
||||
SM,
|
||||
MD,
|
||||
LG,
|
||||
XL,
|
||||
XXL
|
||||
SM = 'SM',
|
||||
MD = 'MD',
|
||||
LG = 'LG',
|
||||
XL = 'XL',
|
||||
XXL = 'XXL'
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// hero.ts
|
||||
import { heroui } from "@heroui/react";
|
||||
export default heroui();
|
||||
@@ -11,20 +11,20 @@ export const useBreakpoints = () => {
|
||||
switch (true) {
|
||||
case width < 640: {
|
||||
size = ScreenSize.SM;
|
||||
console.log('small')
|
||||
|
||||
break;
|
||||
}
|
||||
case width >= 640: {
|
||||
case width >= 640 && width < 1024: {
|
||||
size = ScreenSize.MD;
|
||||
|
||||
break;
|
||||
}
|
||||
case width >= 1024: {
|
||||
case width >= 1024 && width < 1280: {
|
||||
size = ScreenSize.LG
|
||||
|
||||
break;
|
||||
}
|
||||
case width >= 1280: {
|
||||
case width >= 1280 && width < 1536: {
|
||||
size = ScreenSize.XL;
|
||||
|
||||
break;
|
||||
@@ -42,14 +42,10 @@ export const useBreakpoints = () => {
|
||||
const onWindowResize = () => {
|
||||
const width: number = window.innerWidth;
|
||||
const newScreenSize: ScreenSize = getWindowSize(width);
|
||||
console.log(newScreenSize)
|
||||
|
||||
setScreenSize(newScreenSize);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(windowWidth)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
onWindowResize()
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ export const useUserRole = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isUserLoggedIn && decodedIdToken) {
|
||||
const idTokenRoles: string[] = decodedIdToken!.roles as string[];
|
||||
const rolesKeyName: string | undefined = Object.keys(decodedIdToken).find((key) => key.includes('roles'));
|
||||
const idTokenRoles: string[] = decodedIdToken[rolesKeyName!] as string[];
|
||||
|
||||
let newUserRole: string | undefined;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Flying</title>
|
||||
</head>
|
||||
<body>
|
||||
<body style="background-color: #f5f5f5;">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface Alert {
|
||||
severity: 'danger' | 'default' | 'success' | 'warning';
|
||||
severity: 'info' | 'error' | 'success' | 'warning';
|
||||
message: string;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
||||
import LogbookContextProvider from './context/logbookContext/LogbookContextProvider.tsx'
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { OidcProvider } from './auth/oidcConfig.ts';
|
||||
import './styles.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<OidcProvider>
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
@import "tailwindcss";
|
||||
@plugin './hero.ts';
|
||||
/* Note: You may need to change the path to fit your project structure */
|
||||
@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@plugin "daisyui";
|
||||
@plugin "daisyui/theme" {
|
||||
name: "lofi";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(100% 0 0);
|
||||
--color-base-200: oklch(97% 0 0);
|
||||
--color-base-300: oklch(94% 0 0);
|
||||
--color-base-content: oklch(0% 0 0);
|
||||
--color-primary: oklch(15.906% 0 0);
|
||||
--color-primary-content: oklch(100% 0 0);
|
||||
--color-secondary: oklch(21.455% 0.001 17.278);
|
||||
--color-secondary-content: oklch(100% 0 0);
|
||||
--color-accent: oklch(26.861% 0 0);
|
||||
--color-accent-content: oklch(100% 0 0);
|
||||
--color-neutral: oklch(0% 0 0);
|
||||
--color-neutral-content: oklch(100% 0 0);
|
||||
--color-info: oklch(79.54% 0.103 205.9);
|
||||
--color-info-content: oklch(15.908% 0.02 205.9);
|
||||
--color-success: oklch(90.13% 0.153 164.14);
|
||||
--color-success-content: oklch(18.026% 0.03 164.14);
|
||||
--color-warning: oklch(88.37% 0.135 79.94);
|
||||
--color-warning-content: oklch(17.674% 0.027 79.94);
|
||||
--color-error: oklch(78.66% 0.15 28.47);
|
||||
--color-error-content: oklch(15.732% 0.03 28.47);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
--depth: 0;
|
||||
--noise: 0;
|
||||
}
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@theme inline {
|
||||
--color-primary: #000000;
|
||||
body {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
5
client/src/tanstack.d.ts
vendored
5
client/src/tanstack.d.ts
vendored
@@ -3,8 +3,9 @@ import '@tanstack/react-table';
|
||||
/* eslint-disable */
|
||||
declare module '@tanstack/react-table' {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
align?: 'left' | 'center' | 'right';
|
||||
headerAlign?: 'left' | 'center' | 'right';
|
||||
align?: 'text-left' | 'text-center' | 'text-right';
|
||||
className?: string;
|
||||
headerAlign?: 'text-left' | 'text-center' | 'text-right';
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
5
client/src/vite-env.d.ts
vendored
5
client/src/vite-env.d.ts
vendored
@@ -1,10 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_BASE_URL: string;
|
||||
readonly VITE_CLIENT_ID: string;
|
||||
readonly VITE_ISSUER_URI: string;
|
||||
readonly VITE_TENANT_ID: string;
|
||||
readonly VITE_REDIRECT_URL: string;
|
||||
readonly VITE_USERINFO_ENDPOINT: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
dbs:
|
||||
- path: /var/lib/data/flying.db
|
||||
- path: /mnt/data/flying.db
|
||||
replicas:
|
||||
- path: /mnt/backup/flying.db
|
||||
- path: /mnt/data/backup/flying.db
|
||||
@@ -16,11 +16,11 @@ services:
|
||||
container_name: restore
|
||||
image: litestream/litestream:0.3.13
|
||||
volumes:
|
||||
- ./database/flying.db:/var/lib/data/flying.db
|
||||
- ./database/flying.db:/mnt/data/flying.db
|
||||
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
||||
- backup:/mnt/data/backup
|
||||
- data:/var/lib/data
|
||||
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /var/lib/data/flying.db
|
||||
- data:/mnt/data
|
||||
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /mnt/data/flying.db
|
||||
|
||||
app:
|
||||
container_name: flying
|
||||
@@ -31,9 +31,9 @@ services:
|
||||
env_file:
|
||||
- ./api/.env
|
||||
environment:
|
||||
- DB_PATH=../../var/lib/data/flying.db
|
||||
- DB_PATH=../../mnt/data/flying.db
|
||||
volumes:
|
||||
- data:/var/lib/data
|
||||
- data:/mnt/data
|
||||
depends_on:
|
||||
restore:
|
||||
condition: service_completed_successfully
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
volumes:
|
||||
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
||||
- backup:/mnt
|
||||
- data:/var/lib/data
|
||||
- data:/mnt/data
|
||||
command: replicate -config /mnt/litestream/litestream.yml
|
||||
depends_on:
|
||||
app:
|
||||
|
||||
38
infrastructure/.terraform.lock.hcl
generated
38
infrastructure/.terraform.lock.hcl
generated
@@ -1,6 +1,25 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/azuread" {
|
||||
version = "3.7.0"
|
||||
hashes = [
|
||||
"h1:+DHwtiYCOYEhLMot5KQ0hm+zopmUJwPJ/gJiXVfdNOs=",
|
||||
"zh:01114beb07eb8415a563ca80a513e7fd11fd6f0670bdd960e9092e58464ab2b7",
|
||||
"zh:1460c790ca28cc11c3efd4abec076fc5cdd4ae1cc654a45c69e10ca064e0a611",
|
||||
"zh:161b6dd82cf279e7ac7a31c404f2ace491a5a9e7ec382ace91de2fd18f8c190b",
|
||||
"zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7",
|
||||
"zh:2eff0bea92c48be0221863ebf7752f491232088f4ec3cbe4da2ebf9f478fed1f",
|
||||
"zh:69a689198fcac054da30fea6a8abce2f12b667f6f327c9bb388f2c307ee782fd",
|
||||
"zh:6f5acf42d71a2f5157c54e67b2db9b080c4ebb27b812112c0c84241e66d94803",
|
||||
"zh:8e0a58ff2c21aff372c66c679db48578be48e99f5cf6727f082cf4b629dbda6c",
|
||||
"zh:9c88cc06ba7ee1f6e6b43af7e223c4d107fb77e5a984f1441fffb00c3693537f",
|
||||
"zh:e8656e8143baac71029857c72899f8503cede41f2559c89ce29d21c0436c96fc",
|
||||
"zh:e8e21913956a3f043c0e65cc95278bdc82f62ff33fd75a56b1919bd63fd14f8a",
|
||||
"zh:f7f213dd61d1c75b8397d97660cf329870a241e88ec6e0dc07c60725d975252e",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/azurerm" {
|
||||
version = "4.40.0"
|
||||
constraints = "4.40.0"
|
||||
@@ -20,3 +39,22 @@ provider "registry.terraform.io/hashicorp/azurerm" {
|
||||
"zh:f6fd85bfca8516f9f3840acc3f0e5e335f6195b66231c2db05c0fb43dd8aebe1",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/random" {
|
||||
version = "3.7.2"
|
||||
hashes = [
|
||||
"h1:KG4NuIBl1mRWU0KD/BGfCi1YN/j3F7H4YgeeM7iSdNs=",
|
||||
"zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f",
|
||||
"zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc",
|
||||
"zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab",
|
||||
"zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3",
|
||||
"zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212",
|
||||
"zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34",
|
||||
"zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967",
|
||||
"zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d",
|
||||
"zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62",
|
||||
"zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ locals {
|
||||
prod = "flying-prod"
|
||||
}
|
||||
|
||||
app_reg_name = {
|
||||
test = "noahspan-flying-test"
|
||||
prod = "noahspan-flying-prod"
|
||||
}
|
||||
|
||||
app_reg_federated_identity_credential_subject = {
|
||||
test = "repo:noahspannbauer/noahspan-flying:environment:test"
|
||||
prod = "repo:noahspannbauer/noahspan-flying:environment:prod"
|
||||
}
|
||||
|
||||
container_image = {
|
||||
test = "noahspan/flying-app:v2.0.0-alpha"
|
||||
prod = "noahspan/flying-app:v2.0.0-alpha"
|
||||
|
||||
@@ -2,6 +2,14 @@ output "app_name" {
|
||||
value = local.app_name[var.environment]
|
||||
}
|
||||
|
||||
output "app_reg_name" {
|
||||
value = local.app_reg_name[var.environment]
|
||||
}
|
||||
|
||||
output "app_reg_federated_identity_credential_subject" {
|
||||
value = local.app_reg_federated_identity_credential_subject[var.environment]
|
||||
}
|
||||
|
||||
output "container_image" {
|
||||
value = local.container_image[var.environment]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ resource "azurerm_container_app" "container_app" {
|
||||
max_replicas = 2
|
||||
|
||||
init_container {
|
||||
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/data/flying.db"]
|
||||
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/mnt/data/flying.db"]
|
||||
cpu = 0.25
|
||||
image = "litestream/litestream:0.5.2"
|
||||
memory = "0.5Gi"
|
||||
@@ -17,12 +17,12 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
volume_mounts {
|
||||
name = "data"
|
||||
path = "/var/lib/data"
|
||||
path = "/mnt/data"
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
name = "backup"
|
||||
path = "/mnt/data"
|
||||
path = "/mnt/data/backup"
|
||||
sub_path = "data"
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
volume_mounts {
|
||||
name = "data"
|
||||
path = "/var/lib/data"
|
||||
path = "/mnt/data"
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
name = "backup"
|
||||
path = "/mnt/data"
|
||||
path = "/mnt/data/backup"
|
||||
sub_path = "data"
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
container {
|
||||
cpu = 0.25
|
||||
image = "noahspan/flying-app:v2.0.0-alpha"
|
||||
image = "noahspan/flying:20589367895"
|
||||
memory = "0.5Gi"
|
||||
name = "flying"
|
||||
|
||||
@@ -69,9 +69,19 @@ resource "azurerm_container_app" "container_app" {
|
||||
secret_name = "azure-storage-connection-string"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "AUTHORITY"
|
||||
value = var.AUTHORITY
|
||||
}
|
||||
|
||||
env {
|
||||
name = "AUDIENCE"
|
||||
value = var.CLIENT_ID
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CLIENT_ID"
|
||||
secret_name = "client-id"
|
||||
value = var.CLIENT_ID
|
||||
}
|
||||
|
||||
env {
|
||||
@@ -79,25 +89,45 @@ resource "azurerm_container_app" "container_app" {
|
||||
secret_name = "client-secret"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "ISSUER_URL"
|
||||
value = var.ISSUER_URL
|
||||
}
|
||||
|
||||
env {
|
||||
name = "JWKS_URI"
|
||||
value = var.JWKS_URI
|
||||
}
|
||||
|
||||
env {
|
||||
name = "NODE_ENV"
|
||||
value = "test"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "SESSION_SECRET"
|
||||
secret_name = "session-secret"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "TENANT_ID"
|
||||
value = var.TENANT_ID
|
||||
value = var.EXTERNAL_TENANT_ID
|
||||
}
|
||||
|
||||
env {
|
||||
name = "DB_PATH"
|
||||
value = "/var/lib/data/flying.db"
|
||||
value = "/mnt/data/flying.db"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "DB_SYNC"
|
||||
value = "false"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
startup_probe {
|
||||
failure_count_threshold = 10
|
||||
initial_delay = 1
|
||||
interval_seconds = 2
|
||||
failure_count_threshold = 3
|
||||
initial_delay = 15
|
||||
interval_seconds = 30
|
||||
path = "/api/health"
|
||||
port = 3000
|
||||
transport = "HTTP"
|
||||
@@ -105,7 +135,7 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
volume_mounts {
|
||||
name = "data"
|
||||
path = "/var/lib/data"
|
||||
path = "/mnt/data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,11 +174,6 @@ resource "azurerm_container_app" "container_app" {
|
||||
value = azurerm_storage_account.storage_account.primary_connection_string
|
||||
}
|
||||
|
||||
secret {
|
||||
name = "client-id"
|
||||
value = var.CLIENT_ID
|
||||
}
|
||||
|
||||
secret {
|
||||
name = "client-secret"
|
||||
value = var.CLIENT_SECRET
|
||||
@@ -159,6 +184,11 @@ resource "azurerm_container_app" "container_app" {
|
||||
value = var.DOCKER_IO_PASSWORD
|
||||
}
|
||||
|
||||
secret {
|
||||
name = "session-secret"
|
||||
value = var.SESSION_SECRET
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [ template[0].container[0].image, template[0].container[0].image, template[0].init_container[0].image, registry[0].server ]
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ resource "azurerm_container_app_environment_storage" "container_app_environment_
|
||||
account_name = azurerm_storage_account.storage_account.name
|
||||
share_name = azurerm_storage_share.storage_share[0].name
|
||||
access_key = azurerm_storage_account.storage_account.primary_access_key
|
||||
access_mode = "ReadOnly"
|
||||
access_mode = "ReadWrite"
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
provider "azurerm" {
|
||||
features {}
|
||||
}
|
||||
|
||||
provider "random" {}
|
||||
@@ -6,6 +6,14 @@ terraform {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "4.40.0"
|
||||
}
|
||||
|
||||
azuread = {
|
||||
source = "hashicorp/azuread"
|
||||
}
|
||||
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
}
|
||||
}
|
||||
|
||||
backend "remote" {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
variable "AUTHORITY" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "CLIENT_ID" {
|
||||
type = string
|
||||
@@ -17,10 +20,27 @@ variable "DOCKER_IO_USERNAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ISSUER_URL" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "JWKS_URI" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "RESOURCE_GROUP_NAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "EXTERNAL_TENANT_ID" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "SESSION_SECRET" {
|
||||
sensitive = true
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "TENANT_ID" {
|
||||
type = string
|
||||
}
|
||||
|
||||
3936
package-lock.json
generated
3936
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user