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
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: ${{ inputs.environment_name }}
|
environment: ${{ inputs.environment_name }}
|
||||||
steps:
|
steps:
|
||||||
@@ -19,6 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
api
|
api
|
||||||
|
client
|
||||||
|
|
||||||
- name: Install Nest CLI
|
- name: Install Nest CLI
|
||||||
run: |
|
run: |
|
||||||
@@ -37,8 +38,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm run build -w api
|
npm run build -w api
|
||||||
|
|
||||||
- name: Build Client
|
- name: Test API
|
||||||
run: |
|
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
|
npm run build -w client
|
||||||
|
|
||||||
- name: Log into Docker Hub
|
- name: Log into Docker Hub
|
||||||
@@ -59,4 +71,3 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: noahspan/flying:${{ inputs.version_number }}
|
tags: noahspan/flying:${{ inputs.version_number }}
|
||||||
context: .
|
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
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
initial-fetch-depth: 2
|
|
||||||
filters: |
|
filters: |
|
||||||
api:
|
api:
|
||||||
- 'api/**'
|
- 'api/**'
|
||||||
|
|||||||
6
.github/workflows/deploy.yaml
vendored
6
.github/workflows/deploy.yaml
vendored
@@ -23,13 +23,13 @@ jobs:
|
|||||||
- name: Log in to Azure
|
- name: Log in to Azure
|
||||||
uses: azure/login@v2
|
uses: azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ vars.VITE_CLIENT_ID }}
|
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
tenant-id: ${{ vars.VITE_TENANT_ID }}
|
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||||
|
|
||||||
- name: Azure CLI script
|
- name: Azure CLI script
|
||||||
uses: azure/cli@v2
|
uses: azure/cli@v2
|
||||||
with:
|
with:
|
||||||
azcliversion: latest
|
azcliversion: latest
|
||||||
inlineScript: |
|
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
|
uses: ./.github/workflows/changes.yaml
|
||||||
|
|
||||||
build:
|
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
|
name: build
|
||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
uses: ./.github/workflows/build.yaml
|
uses: ./.github/workflows/build_and_test.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: test
|
environment_name: test
|
||||||
version_number: ${{ github.run_id }}
|
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
|
name: build
|
||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
uses: ./.github/workflows/build.yaml
|
uses: ./.github/workflows/build_and_test.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: pull_request
|
environment_name: pull_request
|
||||||
version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
|
|||||||
4
.github/workflows/tag.yaml
vendored
4
.github/workflows/tag.yaml
vendored
@@ -2,12 +2,12 @@ name: Tag
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
- '**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: build
|
name: build
|
||||||
uses: ./.github/workflows/build.yaml
|
uses: ./.github/workflows/build_and_test.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: prod
|
environment_name: prod
|
||||||
version_number: ${{ github.ref_name }}
|
version_number: ${{ github.ref_name }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=linux/amd64 node:22-slim
|
FROM node:22
|
||||||
|
|
||||||
WORKDIR app
|
WORKDIR app
|
||||||
COPY ./api/dist ./api/dist
|
COPY ./api/dist ./api/dist
|
||||||
@@ -9,10 +9,8 @@ WORKDIR api
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY ./api/entrypoint.sh ./entrypoint.sh
|
|
||||||
RUN chmod +x entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
CMD ["node", "./app/api/dist/main.js"]
|
||||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
# 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",
|
"name": "api",
|
||||||
"version": "2.0.0-alpha",
|
"version": "2.1.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"@nestjs/serve-static": "^5.0.3",
|
"@nestjs/serve-static": "^5.0.3",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"@noahspan/azure-database": "^3.1.2",
|
"@noahspan/azure-database": "^3.1.2",
|
||||||
"@noahspan/noahspan-modules": "^1.2.9",
|
"@noahspan/noahspan-modules": "^1.2.11",
|
||||||
"@schematics/angular": "^17.3.7",
|
"@schematics/angular": "^17.3.7",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
|
|||||||
@@ -30,25 +30,26 @@ import { join } from 'path';
|
|||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [configuration]
|
load: [configuration]
|
||||||
}),
|
}),
|
||||||
// HealthModule,
|
HealthModule,
|
||||||
LogModule,
|
LogModule,
|
||||||
PilotModule,
|
|
||||||
ServeStaticModule.forRoot({
|
|
||||||
rootPath: join(__dirname, '../..', 'client', 'dist')
|
|
||||||
}),
|
|
||||||
TrackModule,
|
|
||||||
TypeOrmModule.forRoot(dataSourceOptions),
|
|
||||||
MsGraphModule.registerAsync({
|
MsGraphModule.registerAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
return {
|
return {
|
||||||
|
authority: configService.get<string>('authority'),
|
||||||
clientId: configService.get<string>('clientId'),
|
clientId: configService.get<string>('clientId'),
|
||||||
clientSecret: configService.get<string>('clientSecret'),
|
clientSecret: configService.get<string>('clientSecret'),
|
||||||
tenantId: configService.get<string>('tenantId')
|
tenantId: configService.get<string>('tenantId')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
PilotModule,
|
||||||
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: join(__dirname, '../..', 'client', 'dist')
|
||||||
|
}),
|
||||||
|
TrackModule,
|
||||||
|
TypeOrmModule.forRoot(dataSourceOptions)
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||||
audience: process.env.AUDIENCE,
|
audience: process.env.AUDIENCE,
|
||||||
|
authority: process.env.AUTHORITY,
|
||||||
clientId: process.env.CLIENT_ID,
|
clientId: process.env.CLIENT_ID,
|
||||||
clientSecret: process.env.CLIENT_SECRET,
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
issuer: process.env.ISSUER_URL,
|
issuer: process.env.ISSUER_URL,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export const dataSourceOptions: DataSourceOptions = {
|
|||||||
database: configService.get<string>('DB_PATH'),
|
database: configService.get<string>('DB_PATH'),
|
||||||
entities: ['../**/*.entity.js'],
|
entities: ['../**/*.entity.js'],
|
||||||
migrations: ['./migrations/*.js'],
|
migrations: ['./migrations/*.js'],
|
||||||
synchronize: configService.get<boolean>('DB_SYNC')
|
synchronize: configService.get<boolean>('DB_SYNC'),
|
||||||
|
migrationsRun: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSource = new DataSource(dataSourceOptions);
|
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';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'endorsements' })
|
@Entity({ name: 'endorsements' })
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
return fileUrl;
|
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;
|
this.containerName = containerName;
|
||||||
|
|
||||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
|
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
||||||
const downloadBlockBlobResponse = await blockBlobClient.download();
|
const downloadBlockBlobResponse = await blockBlobClient.download();
|
||||||
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
|
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,25 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from "./health.controller"
|
||||||
import { HealthService } from './health.service';
|
import { HttpStatus } from "@nestjs/common";
|
||||||
|
|
||||||
describe('HealthController', () => {
|
describe('HealthController', () => {
|
||||||
let controller; HealthController;
|
let controller: HealthController;
|
||||||
|
|
||||||
const mockHealthService = {
|
|
||||||
isDatabaseConnected: jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController]
|
||||||
providers: [HealthService]
|
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<HealthController>(HealthController);
|
controller = module.get<HealthController>(HealthController);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isHealthy => should return true', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
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 () => {
|
it('isHealth => should return status ok', async () => {
|
||||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
|
|
||||||
|
|
||||||
const result = await controller.isHealthy();
|
const result = await controller.isHealthy();
|
||||||
|
|
||||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
expect(result).toEqual(HttpStatus.OK);
|
||||||
expect(result).toEqual(false);
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,27 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { HealthService } from './health.service';
|
|
||||||
import { CustomError } from 'src/error/customError';
|
|
||||||
|
|
||||||
|
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(
|
|
||||||
private readonly healthService: HealthService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async isHealthy(): Promise<boolean> {
|
async isHealthy(): Promise<HttpStatus> {
|
||||||
try {
|
return HttpStatus.OK
|
||||||
return await this.healthService.isDatabaseConnected();
|
|
||||||
} catch (error) {
|
|
||||||
const customError = error as CustomError;
|
|
||||||
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
import { HealthService } from './health.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [
|
|
||||||
HealthService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
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,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -19,6 +20,7 @@ import { LogInterceptor } from './log.interceptor';
|
|||||||
import { FileService } from '../file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Logs } from './logs.interface';
|
||||||
|
|
||||||
const reflector = new Reflector();
|
const reflector = new Reflector();
|
||||||
|
|
||||||
@@ -30,44 +32,52 @@ export class LogController {
|
|||||||
private readonly logService: LogService
|
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')
|
@Get(':id')
|
||||||
@Public()
|
@Public()
|
||||||
async find(
|
async find(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
): Promise<LogEntity> {
|
): Promise<LogEntity> {
|
||||||
try {
|
try {
|
||||||
console.log(id)
|
|
||||||
return await this.logService.find(id);
|
return await this.logService.find(id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
console.log(error)
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
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()
|
@Post()
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
async create(@Body() logDto: LogDto) {
|
||||||
try {
|
try {
|
||||||
return await this.logService.create(logDto);
|
return await this.logService.create(logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
console.log(error)
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,14 +87,12 @@ export class LogController {
|
|||||||
async update(
|
async update(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() logDto: LogDto
|
@Body() logDto: LogDto
|
||||||
): Promise<UpdateResult> {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log(id)
|
|
||||||
console.log(logDto)
|
|
||||||
return await this.logService.update(id, logDto);
|
return await this.logService.update(id, logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
console.log(error)
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +105,6 @@ export class LogController {
|
|||||||
try {
|
try {
|
||||||
return await this.logService.delete(id);
|
return await this.logService.delete(id);
|
||||||
} catch (error) {
|
} 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 { PilotEntity } from "src/pilot/pilot.entity";
|
|
||||||
|
|
||||||
export class LogDto {
|
export class LogDto {
|
||||||
pilotId: string;
|
pilotId: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from '../pilot/pilot.entity';
|
||||||
import { TrackEntity } from 'src/track/track.entity';
|
import { TrackEntity } from '../track/track.entity';
|
||||||
|
import { ColumnNumericTransformer } from '../transformers/columnNumeric.transformer';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'logs' })
|
@Entity({ name: 'logs' })
|
||||||
@@ -25,52 +26,131 @@ export class LogEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
routeTo: string;
|
routeTo: string;
|
||||||
|
|
||||||
@Column()
|
@Column('numeric', {
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer()
|
||||||
|
})
|
||||||
durationOfFlight: number;
|
durationOfFlight: number;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
singleEngineLand: number | null;
|
singleEngineLand: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
simulatorAtd: number | null;
|
simulatorAtd: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
landingsDay: number | null;
|
landingsDay: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
landingsNight: number | null;
|
landingsNight: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
groundTrainingReceived: number | null;
|
groundTrainingReceived: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
flightTrainingReceived: number | null;
|
flightTrainingReceived: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
crossCountry: number | null;
|
crossCountry: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
night: number | null;
|
night: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
solo: number | null;
|
solo: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
pilotInCommand: number | null;
|
pilotInCommand: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
instrumentActual: number | null;
|
instrumentActual: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
instrumentSimulated: number | null;
|
instrumentSimulated: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
instrumentApproaches: number | null;
|
instrumentApproaches: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
instrumentHolds: number | null;
|
instrumentHolds: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column('numeric', {
|
||||||
|
nullable: true,
|
||||||
|
precision: 10,
|
||||||
|
scale: 1,
|
||||||
|
transformer: new ColumnNumericTransformer(),
|
||||||
|
})
|
||||||
instrumentNavTrack: number | null;
|
instrumentNavTrack: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException }
|
|||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
import { LogEntity } from './log.entity';
|
import { LogEntity } from './log.entity';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
|
|
||||||
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
@@ -16,42 +15,62 @@ export class LogInterceptor implements NestInterceptor {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return handler.handle().pipe(
|
return handler.handle().pipe(
|
||||||
map((data: LogEntity[]) => {
|
map((data: any) => {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const limitData = (data) => {
|
const limitData = (log: LogEntity) => {
|
||||||
return data.map((log: LogEntity) => {
|
return {
|
||||||
return {
|
id: log.id,
|
||||||
id: log.id,
|
pilot: {
|
||||||
pilot: {
|
name: log.pilot.name
|
||||||
name: log.pilot.name
|
},
|
||||||
},
|
date: log.date,
|
||||||
date: log.date,
|
aircraftMakeModel: log.aircraftMakeModel,
|
||||||
aircraftMakeModel: log.aircraftMakeModel,
|
routeFrom: log.routeFrom,
|
||||||
routeFrom: log.routeFrom,
|
routeTo: log.routeTo,
|
||||||
routeTo: log.routeTo,
|
durationOfFlight: log.durationOfFlight,
|
||||||
durationOfFlight: log.durationOfFlight,
|
tracks: log.tracks,
|
||||||
tracks: log.tracks,
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers.authorization) {
|
if (req.headers.authorization) {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
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')) {
|
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
||||||
const logs = limitData(data);
|
if (data.entities) {
|
||||||
|
const logs = data.entities.map((entity) => limitData(data.entities));
|
||||||
|
|
||||||
return logs;
|
return {
|
||||||
|
entities: logs,
|
||||||
|
total: data.total,
|
||||||
|
hasNextPage: data.hasNextPage
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const log = limitData(data);
|
||||||
|
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} else if (!req.headers.authorization && isPublic) {
|
} else if (!req.headers.authorization && isPublic) {
|
||||||
const publicData = limitData(data)
|
if (data.entities) {
|
||||||
const logs = publicData.slice(0, 5)
|
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 { ConfigService } from '@nestjs/config';
|
||||||
import { FileService } from '../file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { PilotModule } from 'src/pilot/pilot.module';
|
import { PilotModule } from '../pilot/pilot.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
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 { LogEntity } from './log.entity';
|
||||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||||
import { LogDto } from './log.dto';
|
import { LogDto } from './log.dto';
|
||||||
import { PilotService } from 'src/pilot/pilot.service';
|
import { PilotService } from '../pilot/pilot.service';
|
||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from '../pilot/pilot.entity';
|
||||||
import { CustomError } from 'src/error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
import { FileService } from 'src/file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
|
import { Logs } from './logs.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogService {
|
export class LogService {
|
||||||
@@ -17,42 +18,65 @@ export class LogService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async find(id: string): Promise<LogEntity> {
|
async find(id: string): Promise<LogEntity> {
|
||||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
return await this.logRepository.findOne({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
relations: ['pilot', 'tracks']
|
relations: ['pilot', 'tracks']
|
||||||
});
|
});
|
||||||
|
|
||||||
return logEntity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(): Promise<LogEntity[]> {
|
async findLogsWithCount(skip?: number, take?: number): Promise<Logs> {
|
||||||
return await this.logRepository.find({
|
// const [entities, total] = await this.logRepository.findAndCount({
|
||||||
relations: ['pilot', 'tracks']
|
// take,
|
||||||
});
|
// skip,
|
||||||
}
|
// order: {
|
||||||
|
// date: 'DESC'
|
||||||
|
// },
|
||||||
|
// relations: ['pilot', 'tracks']
|
||||||
|
// })
|
||||||
|
|
||||||
async create(logDto: LogDto): Promise<InsertResult> {
|
const [entities, total] = await this.logRepository
|
||||||
try{
|
.createQueryBuilder('logs')
|
||||||
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
|
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||||
|
.orderBy('logs.date', 'DESC')
|
||||||
|
.skip(skip)
|
||||||
|
.take(take)
|
||||||
|
.getManyAndCount()
|
||||||
|
|
||||||
if (pilotEntity) {
|
return {
|
||||||
const { pilotId, ...newLogDto } = logDto;
|
entities,
|
||||||
const log = this.logRepository.create({
|
total,
|
||||||
...newLogDto,
|
hasNextPage: skip + take < total
|
||||||
pilot: pilotEntity
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.logRepository.insert(log);
|
|
||||||
} else {
|
|
||||||
throw new CustomError('Pilot not found', 'Not found', 404);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, log: LogDto): Promise<UpdateResult> {
|
async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
|
||||||
return await this.logRepository.update(id, log);
|
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> {
|
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 httpService = new HttpService();
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors();
|
||||||
origin: 'http://localhost:8080', // Allow requests from your frontend's origin
|
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
||||||
credentials: true, // If you need to send cookies or authorization headers
|
|
||||||
});
|
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret: 'blah',
|
secret: process.env.SESSION_SECRET,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: 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';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'medical' })
|
@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();
|
return await this.pilotService.findAll();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
console.log(error)
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Body() pilotDto: PilotDto) {
|
async create(@Body() pilotDto: PilotDto): Promise<PilotDto> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
return await this.pilotService.create(pilotDto);
|
return await this.pilotService.create(pilotDto);
|
||||||
} catch (error) {
|
} 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 {
|
export class PilotDto {
|
||||||
name: string;
|
name: string;
|
||||||
address: 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 { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||||
import { CertificateEntity } from '../certificate/certificate.entity';
|
import { CertificateEntity } from '../certificate/certificate.entity';
|
||||||
import { EndorsementEntity } from 'src/endorsement/endorsement.entity';
|
import { EndorsementEntity } from '../endorsement/endorsement.entity';
|
||||||
import { MedicalEntity } from 'src/medical/medical.entity';
|
import { MedicalEntity } from '../medical/medical.entity';
|
||||||
|
|
||||||
@Entity({ name: 'pilots' })
|
@Entity({ name: 'pilots' })
|
||||||
export class PilotEntity {
|
export class PilotEntity {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
|
|
||||||
import { PilotEntity } from './pilot.entity';
|
import { PilotEntity } from './pilot.entity';
|
||||||
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
@@ -26,13 +25,15 @@ export class PilotInterceptor implements NestInterceptor {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log(req.headers.authorization)
|
|
||||||
if (req.headers.authorization) {
|
if (req.headers.authorization) {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
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)
|
const pilots = limitData(data)
|
||||||
|
|
||||||
return pilots;
|
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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||||
import { PilotDto } from './pilot.dto';
|
import { PilotDto } from './pilot.dto';
|
||||||
import { CustomError } from 'src/error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PilotService {
|
export class PilotService {
|
||||||
@@ -13,7 +13,7 @@ export class PilotService {
|
|||||||
|
|
||||||
async find(id: string): Promise<PilotEntity> {
|
async find(id: string): Promise<PilotEntity> {
|
||||||
try {
|
try {
|
||||||
const pilotEntity = await this.pilotRepository.findOneBy({ id });
|
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||||
|
|
||||||
if (pilotEntity) {
|
if (pilotEntity) {
|
||||||
return pilotEntity
|
return pilotEntity
|
||||||
@@ -29,15 +29,18 @@ export class PilotService {
|
|||||||
return await this.pilotRepository.find();
|
return await this.pilotRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(pilot: PilotDto): Promise<InsertResult> {
|
async create(pilot: PilotDto): Promise<PilotDto> {
|
||||||
return await this.pilotRepository.insert(pilot);
|
return await this.pilotRepository.save(pilot);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
id: string,
|
id: string,
|
||||||
pilot: PilotDto
|
pilotDto: PilotDto
|
||||||
): Promise<UpdateResult> {
|
): Promise<PilotDto> {
|
||||||
return await this.pilotRepository.update(id, pilot);
|
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> {
|
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';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'tracks' })
|
@Entity({ name: 'tracks' })
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TrackEntity } from "./track.entity";
|
|||||||
import { TrackController } from "./track.controller";
|
import { TrackController } from "./track.controller";
|
||||||
import { FileService } from "../file/file.service";
|
import { FileService } from "../file/file.service";
|
||||||
import { TrackService } from './track.service';
|
import { TrackService } from './track.service';
|
||||||
import { LogModule } from 'src/log/log.module';
|
import { LogModule } from '../log/log.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
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 { TrackEntity } from "./track.entity";
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { LogService } from "src/log/log.service";
|
import { LogService } from "../log/log.service";
|
||||||
import { LogEntity } from "src/log/log.entity";
|
import { LogEntity } from "../log/log.entity";
|
||||||
import { CustomError } from "src/error/customError";
|
import { CustomError } from "../error/customError";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "../file/file.service";
|
||||||
|
import { Logs } from "src/log/logs.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrackService {
|
export class TrackService {
|
||||||
@@ -19,11 +20,7 @@ export class TrackService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async find(id: string): Promise<TrackEntity> {
|
async find(id: string): Promise<TrackEntity> {
|
||||||
try {
|
return await this.trackRepository.findOneBy({ id });
|
||||||
return await this.trackRepository.findOneBy({ id });
|
|
||||||
} catch (error) {
|
|
||||||
throw new CustomError('Track not found', 'Not found', 404)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(logId: string): Promise<TrackEntity[]> {
|
async findAll(logId: string): Promise<TrackEntity[]> {
|
||||||
@@ -40,10 +37,11 @@ export class TrackService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return tracks;
|
return tracks;
|
||||||
|
} else {
|
||||||
|
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
throw error
|
||||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,21 +67,11 @@ export class TrackService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
||||||
try {
|
return await this.trackRepository.update(id, track);
|
||||||
return await this.trackRepository.update(id, track);
|
|
||||||
} catch(error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
||||||
try {
|
return await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||||
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
|
||||||
|
|
||||||
return downloadedFile;
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
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",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0-alpha",
|
"version": "2.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -14,11 +14,10 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/react-fontawesome": "^3.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",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"daisyui": "^5.1.10",
|
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"oidc-spa": "^7.2.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 Flights from './components/flights/Flights';
|
||||||
import Logbook from './components/logbook/Logbook';
|
import Logbook from './components/logbook/Logbook';
|
||||||
import Pilots from './components/pilots/Pilots';
|
import Pilots from './components/pilots/Pilots';
|
||||||
import SiteNav from './components/siteNav/SiteNav';
|
import SiteNav from './components/siteNav/SiteNav';
|
||||||
import { HeroUIProvider } from '@heroui/react';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useHref, useNavigate } from 'react-router-dom';
|
|
||||||
import './styles.css';
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
<div className='bg-[#f5f5f5]' data-theme="lofi">
|
||||||
<SiteNav />
|
<SiteNav />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Flights />} />
|
<Route path='/' element={<Flights />} />
|
||||||
<Route path="/logbook" element={<Logbook />} />
|
<Route path="/logbook" element={<Logbook />} />
|
||||||
<Route path="/pilots" element={<Pilots />} />
|
<Route path="/pilots" element={<Pilots />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HeroUIProvider>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createReactOidc } from "oidc-spa/react";
|
import { createReactOidc } from "oidc-spa/react";
|
||||||
|
|
||||||
export const { OidcProvider, useOidc, getOidc } = createReactOidc(async () => ({
|
export const { OidcProvider, useOidc, getOidc } = createReactOidc(async () => ({
|
||||||
issuerUri: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}/v2.0`,
|
issuerUri: import.meta.env.VITE_ISSUER_URI,
|
||||||
clientId: import.meta.env.VITE_CLIENT_APP_ID,
|
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||||
homeUrl: import.meta.env.BASE_URL,
|
homeUrl: import.meta.env.VITE_BASE_URL,
|
||||||
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`],
|
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
|
||||||
autoLogin: false,
|
autoLogin: false,
|
||||||
postLoginRedirectUrl: '/',
|
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 { DialogConfirmationProps } from './ConfirmationDialogProps.interface';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
@@ -21,31 +11,25 @@ const ConfirmationDialog = ({
|
|||||||
title
|
title
|
||||||
}: DialogConfirmationProps) => {
|
}: DialogConfirmationProps) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<>
|
||||||
isDismissable={false}
|
<input type='checkbox' id='dialog' className='modal-toggle' onChange={() => {}} checked={isOpen} />
|
||||||
isKeyboardDismissDisabled={true}
|
<div className='modal' role='dialog'>
|
||||||
isOpen={isOpen}
|
<div className='modal-box'>
|
||||||
>
|
<h3 className="text-lg font-bold">{title}</h3>
|
||||||
<ModalContent>
|
<p className="py-4">{contentText}</p>
|
||||||
<ModalHeader>{title}</ModalHeader>
|
<div className='modal-action'>
|
||||||
<ModalBody>
|
<button className='btn' onClick={onCancel}>
|
||||||
{!isLoading && <div>{contentText}</div>}
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
{isLoading && <Spinner size='lg' />}
|
No
|
||||||
</ModalBody>
|
</button>
|
||||||
<ModalFooter>
|
<button className='btn btn-primary' onClick={onConfirm}>
|
||||||
<Button onPress={onCancel} startContent={<FontAwesomeIcon icon={faXmark} />}>
|
<FontAwesomeIcon icon={faCircleCheck} />
|
||||||
No
|
Yes
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
</div>
|
||||||
color='primary'
|
</div>
|
||||||
onPress={onConfirm}
|
</div>
|
||||||
startContent={<FontAwesomeIcon icon={faCircleCheck} />}
|
</>
|
||||||
>
|
|
||||||
Yes
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +1,104 @@
|
|||||||
import { Icon, IconName, Skeleton } from "@noahspan/noahspan-components";
|
|
||||||
import { Card } from '@heroui/react';
|
|
||||||
import LogbookCard from "../logbookCard/LogbookCard";
|
import LogbookCard from "../logbookCard/LogbookCard";
|
||||||
import { useEffect, useReducer } from "react";
|
import { useEffect, useReducer } from "react";
|
||||||
import { useLogs } from "../../hooks/logs/UseLogs";
|
import { useLogs } from "../../hooks/logs/UseLogs";
|
||||||
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
import { initialState, reducer } from "./reducer";
|
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 Flights = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const { logs, logsLoading } = useLogs();
|
const { isUserLoggedIn } = useOidc();
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
|
|
||||||
useEffect(() => {
|
const getFlights = async (pageIndex: number, pageSize: number) => {
|
||||||
const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => {
|
try {
|
||||||
if (log.tracks && log.tracks.length > 0) {
|
const response: AxiosResponse = await httpClient.get(`api/logs/flights`, {
|
||||||
return log;
|
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({
|
||||||
dispatch({ type: 'SET_FLIGHTS', payload: flights})
|
type: 'SET_ALERT',
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
payload: { severity: 'error', message: `Loading of flights failed with the following message: ${axiosError.message}`}
|
||||||
} else {
|
});
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No flights found' }})
|
} finally {
|
||||||
|
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
||||||
}
|
}
|
||||||
}, [logs])
|
}
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
getFlights(state.pageIndex, state.pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(logsLoading)
|
getFlights(state.pageIndex, state.pageSize);
|
||||||
}, [logsLoading])
|
}, [])
|
||||||
|
|
||||||
return (
|
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'>
|
<div className='prose mt-5 mb-5'>
|
||||||
<h1>Flights</h1>
|
<h1>Flights</h1>
|
||||||
</div>
|
</div>
|
||||||
{!logsLoading && state.alert && (
|
{!state.isLoading && state.alert && (
|
||||||
<div>
|
<div>
|
||||||
<Alert
|
<Alert
|
||||||
|
className='mb-5'
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
color={state.alert.severity}
|
severity={state.alert.severity}
|
||||||
title={state.alert.message}
|
>
|
||||||
/>
|
{state.alert.message}
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!logsLoading &&
|
{!state.isLoading &&
|
||||||
<div>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
{logsLoading && [...Array(6)].map((_element, index) => {
|
{state.isLoading && [...Array(5)].map((_element, index) => {
|
||||||
return (
|
return (
|
||||||
<div className='mb-5'>
|
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||||
<Card
|
<div className='card-body' key={index}>
|
||||||
key={index}
|
<div className='skeleton h-10 w-[150px]' />
|
||||||
>
|
<div className='skeleton h-[350px]' />
|
||||||
<div className='p-5'>
|
<div className='skeleton h-[65px]' />
|
||||||
<div className='mb-2'><Skeleton height='h-[60px]' width='w-[200px]' /></div>
|
</div>
|
||||||
<div className='mb-2'><Skeleton height='h-[30px]' width='w-[200px]' /></div>
|
|
||||||
<div className='mb-2'><Skeleton height='h-[300px]' width='w-[300px]' /></div>
|
|
||||||
{[...Array(4)].map((_element, index) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
|
||||||
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ export interface FlightsState {
|
|||||||
alert: Alert | undefined;
|
alert: Alert | undefined;
|
||||||
flights: LogbookEntry[];
|
flights: LogbookEntry[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hasMoreFlights: boolean;
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number
|
||||||
|
totalFlights: number;
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,17 @@ import { FlightsState } from "./FlightsState.interface";
|
|||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { 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 }
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
|
|
||||||
export const initialState: FlightsState = {
|
export const initialState: FlightsState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
flights: [],
|
flights: [],
|
||||||
isLoading: true
|
isLoading: true,
|
||||||
|
hasMoreFlights: true,
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 5,
|
||||||
|
totalFlights: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
@@ -28,7 +32,10 @@ export const reducer = (
|
|||||||
case 'SET_FLIGHTS': {
|
case 'SET_FLIGHTS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
flights: action.payload
|
flights: action.payload.flights,
|
||||||
|
hasMoreFlights: action.payload.hasMoreFlights,
|
||||||
|
pageIndex: action.payload.pageIndex,
|
||||||
|
totalFlights: action.payload.totalFlights
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'SET_IS_LOADING': {
|
case 'SET_IS_LOADING': {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useReducer } from 'react';
|
import { 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, useFormContext } from 'react-hook-form';
|
||||||
import { useForm, Controller, FormProvider, useFormContext } from 'react-hook-form';
|
|
||||||
import { LogFormProps } from './LogFormProps.interface';
|
import { LogFormProps } from './LogFormProps.interface';
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
@@ -10,7 +9,6 @@ import { useOidc } from '../../auth/oidcConfig';
|
|||||||
import httpClient from '../../httpClient/httpClient';
|
import httpClient from '../../httpClient/httpClient';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons'
|
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { parseAbsolute, parseDate, getLocalTimeZone, CalendarDate, ZonedDateTime } from '@internationalized/date';
|
|
||||||
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
||||||
|
|
||||||
const LogForm = () => {
|
const LogForm = () => {
|
||||||
@@ -40,7 +38,7 @@ const LogForm = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
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 {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||||
}
|
}
|
||||||
@@ -55,11 +53,16 @@ const LogForm = () => {
|
|||||||
if (pilots && FormMode.ADD) {
|
if (pilots && FormMode.ADD) {
|
||||||
const newPilotsOptions = pilots.map((pilot) => {
|
const newPilotsOptions = pilots.map((pilot) => {
|
||||||
return {
|
return {
|
||||||
key: pilot.id,
|
|
||||||
label: pilot.name,
|
label: pilot.name,
|
||||||
|
value: pilot.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
console.log(newPilotsOptions)
|
|
||||||
|
newPilotsOptions.unshift({
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
})
|
||||||
|
|
||||||
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
||||||
}
|
}
|
||||||
}, [pilots]);
|
}, [pilots]);
|
||||||
@@ -73,25 +76,21 @@ const LogForm = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="pilotId"
|
name="pilotId"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<select
|
||||||
|
className='select w-full'
|
||||||
aria-labelledby='pilot'
|
aria-labelledby='pilot'
|
||||||
isDisabled={state.isDisabled}
|
disabled={state.isDisabled}
|
||||||
fullWidth={true}
|
onChange={onChange}
|
||||||
isRequired={true}
|
value={[value]}
|
||||||
onSelectionChange={(keys: SharedSelection) => {
|
|
||||||
setValue('pilotId', keys.currentKey);
|
|
||||||
}}
|
|
||||||
selectedKeys={[value]}
|
|
||||||
size='lg'
|
|
||||||
>
|
>
|
||||||
{state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => {
|
{state.pilotOptions?.map((pilotOption: { label: string, value: string; }) => {
|
||||||
return (
|
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"
|
name="date"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const parsedAbsoluteDate = value ? parseAbsolute(value, getLocalTimeZone()) : value
|
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<DatePicker
|
<input
|
||||||
aria-labelledby='date'
|
type='date'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
isRequired={true}
|
disabled={state.isDisabled}
|
||||||
onChange={(selectedDate) => {
|
onChange={onChange}
|
||||||
let date = selectedDate as CalendarDate;
|
value={value ? value.split('T')[0] : ''}
|
||||||
|
/>
|
||||||
setValue('date', date.toDate(getLocalTimeZone()).toISOString())
|
|
||||||
}}
|
|
||||||
size='lg'
|
|
||||||
value={parsedAbsoluteDate ? new CalendarDate(parsedAbsoluteDate.year, parsedAbsoluteDate.month, parsedAbsoluteDate.day) : value}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -131,18 +123,11 @@ const LogForm = () => {
|
|||||||
name="aircraftMakeModel"
|
name="aircraftMakeModel"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Input
|
<input
|
||||||
aria-labelledby='aircraftMakeModel'
|
type='text'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isRequired={true}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
size='lg'
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -158,18 +143,11 @@ const LogForm = () => {
|
|||||||
name="aircraftIdentity"
|
name="aircraftIdentity"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Input
|
<input
|
||||||
aria-labelledby='aircraftIdentity'
|
type='text'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isRequired={true}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
size='lg'
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -185,18 +163,11 @@ const LogForm = () => {
|
|||||||
name="routeFrom"
|
name="routeFrom"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Input
|
<input
|
||||||
aria-labelledby='routeFrom'
|
type='text'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isRequired={true}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
size='lg'
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -210,18 +181,11 @@ const LogForm = () => {
|
|||||||
name="routeTo"
|
name="routeTo"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Input
|
<input
|
||||||
aria-labelledby='routeTo'
|
type='text'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isRequired={true}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
size='lg'
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -235,21 +199,11 @@ const LogForm = () => {
|
|||||||
name="durationOfFlight"
|
name="durationOfFlight"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='durationOfFlight'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isRequired={true}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -265,21 +219,11 @@ const LogForm = () => {
|
|||||||
name="singleEngineLand"
|
name="singleEngineLand"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='singleEngineLand'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isRequired={true}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -293,20 +237,11 @@ const LogForm = () => {
|
|||||||
name="simulatorAtd"
|
name="simulatorAtd"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='simulaterAtd'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
disabled={state.isDisabled}
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -323,22 +258,11 @@ const LogForm = () => {
|
|||||||
name="landingsDay"
|
name="landingsDay"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='landingsDay'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -352,22 +276,11 @@ const LogForm = () => {
|
|||||||
name="landingsNight"
|
name="landingsNight"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='landingsNight'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -386,24 +299,12 @@ const LogForm = () => {
|
|||||||
name="groundTrainingReceived"
|
name="groundTrainingReceived"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='groundTrainingReceived'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
width='w-full'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -418,22 +319,11 @@ const LogForm = () => {
|
|||||||
name="flightTrainingReceived"
|
name="flightTrainingReceived"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='flightTrainingReceived'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -447,22 +337,11 @@ const LogForm = () => {
|
|||||||
name="crossCountry"
|
name="crossCountry"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='crossCountry'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -476,22 +355,11 @@ const LogForm = () => {
|
|||||||
name="night"
|
name="night"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='night'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -505,22 +373,11 @@ const LogForm = () => {
|
|||||||
name="solo"
|
name="solo"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='solo'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -534,22 +391,11 @@ const LogForm = () => {
|
|||||||
name="pilotInCommand"
|
name="pilotInCommand"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='pilotInCommand'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -566,22 +412,11 @@ const LogForm = () => {
|
|||||||
name="instrumentActual"
|
name="instrumentActual"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='instrumentActual'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -595,22 +430,11 @@ const LogForm = () => {
|
|||||||
name="instrumentSimulated"
|
name="instrumentSimulated"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='instrumentSimulated'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -626,22 +450,11 @@ const LogForm = () => {
|
|||||||
name="instrumentApproaches"
|
name="instrumentApproaches"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='instrumentApproaches'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -655,22 +468,11 @@ const LogForm = () => {
|
|||||||
name="instrumentHolds"
|
name="instrumentHolds"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='instrumentHolds'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
|
||||||
}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -684,23 +486,13 @@ const LogForm = () => {
|
|||||||
name="instrumentNavTrack"
|
name="instrumentNavTrack"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<NumberInput
|
<input
|
||||||
aria-labelledby='instrumentNavTrack'
|
type='number'
|
||||||
isDisabled={state.isDisabled}
|
className='input w-full'
|
||||||
color={
|
disabled={state.isDisabled}
|
||||||
formState.errors.address ? 'danger' : undefined
|
min={0}
|
||||||
}
|
onChange={(event) => onChange(Number(event.target.value))}
|
||||||
errorMessage={
|
step={0.1}
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
fullWidth={true}
|
|
||||||
isWheelDisabled={state.isDisabled}
|
|
||||||
onChange={onChange}
|
|
||||||
radius='lg'
|
|
||||||
size='sm'
|
|
||||||
type="number"
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -716,19 +508,12 @@ const LogForm = () => {
|
|||||||
name="notes"
|
name="notes"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Textarea
|
<textarea
|
||||||
aria-labelledby='notes'
|
className='textarea w-full'
|
||||||
isDisabled={state.isDisabled}
|
disabled={state.isDisabled}
|
||||||
color={formState.errors.address ? 'danger' : undefined}
|
|
||||||
errorMessage={
|
|
||||||
formState.errors.address
|
|
||||||
? formState.errors.address.message?.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
fullWidth={true}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
></textarea>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Selection } from "@heroui/react";
|
|
||||||
import { Alert } from "../../interfaces/Alert.interface";
|
import { Alert } from "../../interfaces/Alert.interface";
|
||||||
|
|
||||||
export interface LogFormState {
|
export interface LogFormState {
|
||||||
alert: Alert | undefined;
|
alert: Alert | undefined;
|
||||||
experienceSelectedKeys: Selection;
|
|
||||||
instrumentSelectedKeys: Selection;
|
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
landingsSelectedKeys: Selection;
|
pilotOptions: { label: string; value: string; }[];
|
||||||
pilotOptions: { key: string; label: string; }[];
|
|
||||||
selectedPilotName: string;
|
selectedPilotName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { LogFormState } from './LogFormState.interface';
|
import { LogFormState } from './LogFormState.interface';
|
||||||
import { Selection } from '@heroui/react';
|
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { 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_DISABLED'; payload: boolean }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
| { type: 'SET_LANDINGS_SELECTED_KEYS'; payload: Selection }
|
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
||||||
| { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] }
|
|
||||||
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
||||||
|
|
||||||
export const initialState: LogFormState = {
|
export const initialState: LogFormState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
experienceSelectedKeys: new Set([]),
|
|
||||||
instrumentSelectedKeys: new Set([]),
|
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
landingsSelectedKeys: new Set(['1']),
|
|
||||||
pilotOptions: [],
|
pilotOptions: [],
|
||||||
selectedPilotName: ''
|
selectedPilotName: ''
|
||||||
};
|
};
|
||||||
@@ -34,36 +27,18 @@ export const reducer = (
|
|||||||
alert: action.payload
|
alert: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_EXPERIENCE_SELECTED_KEYS': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
experienceSelectedKeys: action.payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'SET_IS_DISABLED': {
|
case 'SET_IS_DISABLED': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isDisabled: action.payload
|
isDisabled: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_INSTRUMENT_SELECTED_KEYS': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
instrumentSelectedKeys: action.payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'SET_IS_LOADING': {
|
case 'SET_IS_LOADING': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isLoading: action.payload
|
isLoading: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_LANDINGS_SELECTED_KEYS': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
landingsSelectedKeys: action.payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'SET_PILOT_OPTIONS': {
|
case 'SET_PILOT_OPTIONS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Key, useEffect, useReducer } from 'react';
|
import { useEffect, useReducer, useState } from 'react';
|
||||||
import LogForm from '../logForm/LogForm';
|
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { authColumns, unauthColumns } from './columns';
|
|
||||||
import ActionMenu from '../actionMenu/ActionMenu';
|
|
||||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||||
import { LogbookEntry } from './LogbookEntry.interface';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
import LogbookCard from '../logbookCard/LogbookCard';
|
import LogbookCard from '../logbookCard/LogbookCard';
|
||||||
@@ -14,21 +11,42 @@ import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
|||||||
import { UserRole } from '../../enums/userRole';
|
import { UserRole } from '../../enums/userRole';
|
||||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||||
import { ScreenSize } from '../../enums/screenSize';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
|
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, flexRender, getCoreRowModel, HeaderContext, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
||||||
import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
|
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 Logbook: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const logbookContext = useLogbookContext()
|
const [columnVisibility, setColumnVisibility] = useState({});
|
||||||
|
const logbookContext = useLogbookContext();
|
||||||
const { isUserLoggedIn } = useOidc();
|
const { isUserLoggedIn } = useOidc();
|
||||||
const { userRole } = useUserRole();
|
const { userRole } = useUserRole();
|
||||||
const { screenSize } = useBreakpoints();
|
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 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));
|
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;
|
let total: number = 0;
|
||||||
|
|
||||||
@@ -54,7 +72,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
accessorKey: 'date',
|
accessorKey: 'date',
|
||||||
header: 'Date',
|
header: 'Date',
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
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()}`
|
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
@@ -69,13 +87,17 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'route',
|
id: 'route',
|
||||||
header: 'Route of Flight',
|
header: 'Route of Flight',
|
||||||
meta: {
|
meta: {
|
||||||
headerAlign: 'center'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-center'
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
id: 'routeFrom',
|
id: 'routeFrom',
|
||||||
accessorKey: 'routeFrom',
|
accessorKey: 'routeFrom',
|
||||||
header: 'From'
|
header: 'From',
|
||||||
|
meta: {
|
||||||
|
className: 'border-l border-base-300',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'routeTo',
|
id: 'routeTo',
|
||||||
@@ -89,8 +111,9 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
accessorKey: 'durationOfFlight',
|
accessorKey: 'durationOfFlight',
|
||||||
header: 'Duration Of Flight',
|
header: 'Duration Of Flight',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
@@ -99,65 +122,27 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
const notes: ColumnDef<LogbookEntry> = {
|
const notes: ColumnDef<LogbookEntry> = {
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
accessorKey: 'notes',
|
accessorKey: 'notes',
|
||||||
header: 'Notes'
|
header: 'Notes',
|
||||||
|
meta: {
|
||||||
|
className: 'border-l border-base-300',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const actions: ColumnDef<LogbookEntry> = {
|
const actions: ColumnDef<LogbookEntry> = {
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'center',
|
align: 'text-center',
|
||||||
headerAlign: 'center'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-center'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Actions id={info.row.original.id} />
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
const columns: ColumnDef<LogbookEntry>[] = [
|
||||||
pilotName,
|
|
||||||
date,
|
|
||||||
aircraftMakeModel,
|
|
||||||
route,
|
|
||||||
durationOfFlight,
|
|
||||||
notes
|
|
||||||
]
|
|
||||||
|
|
||||||
const authColumns: ColumnDef<LogbookEntry>[] = [
|
|
||||||
pilotName,
|
pilotName,
|
||||||
date,
|
date,
|
||||||
aircraftMakeModel,
|
aircraftMakeModel,
|
||||||
@@ -173,8 +158,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
accessorKey: 'singleEngineLand',
|
accessorKey: 'singleEngineLand',
|
||||||
header: 'Single Engine Land',
|
header: 'Single Engine Land',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
@@ -184,7 +169,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'landings',
|
id: 'landings',
|
||||||
header: 'Landings',
|
header: 'Landings',
|
||||||
meta: {
|
meta: {
|
||||||
headerAlign: 'center'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-center'
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -193,8 +179,9 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Day',
|
header: 'Day',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-right'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -203,8 +190,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Night',
|
header: 'Night',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -213,7 +200,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'instrument',
|
id: 'instrument',
|
||||||
header: 'Instrument',
|
header: 'Instrument',
|
||||||
meta: {
|
meta: {
|
||||||
headerAlign: 'center'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-center'
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -222,8 +210,9 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Actual',
|
header: 'Actual',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
@@ -234,8 +223,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Simulated',
|
header: 'Simulated',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -246,8 +235,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Approaches',
|
header: 'Approaches',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,8 +245,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Holds',
|
header: 'Holds',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -266,8 +255,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Nav/Track',
|
header: 'Nav/Track',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -276,7 +265,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'experienceTraining',
|
id: 'experienceTraining',
|
||||||
header: 'Type of pilot experience or training',
|
header: 'Type of pilot experience or training',
|
||||||
meta: {
|
meta: {
|
||||||
headerAlign: 'center'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-center'
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -285,8 +275,9 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Ground Training Received',
|
header: 'Ground Training Received',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
className: 'border-l border-base-300',
|
||||||
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -297,8 +288,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Flight Training Received',
|
header: 'Flight Training Received',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -309,8 +300,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Cross Country',
|
header: 'Cross Country',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -321,8 +312,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Night',
|
header: 'Night',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -333,8 +324,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Solo',
|
header: 'Solo',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -345,8 +336,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
header: 'Pilot In Command',
|
header: 'Pilot In Command',
|
||||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'text-right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'text-right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
@@ -355,31 +346,87 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
},
|
},
|
||||||
notes,
|
notes,
|
||||||
actions
|
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 {
|
try {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||||
|
|
||||||
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
const response: AxiosResponse = await httpClient.get(`api/logs`, {
|
||||||
const entries: LogbookEntry[] = response.data;
|
params: {
|
||||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
skip: pageIndex * pageSize,
|
||||||
|
take: pageSize
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
if (response.data.entities.length > 0) {
|
||||||
dispatch({ type: 'SET_ENTRIES', payload: response.data });
|
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})
|
dispatch({ type: 'SET_ALERT', payload: undefined})
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
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 {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||||
@@ -431,13 +478,13 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
payload: false
|
payload: false
|
||||||
});
|
});
|
||||||
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
||||||
await getLogbookEntries();
|
await getLogbookEntries(state.pagination?.pageIndex, state.pagination?.pageSize);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
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 {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
||||||
@@ -451,181 +498,269 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// useEffect(() => {
|
const onRowsPerPageChanged = (event: any) => {
|
||||||
// let newColumns: ColumnDef<LogbookEntry>[];
|
const newPaginationState: PaginationState = {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries
|
||||||
|
};
|
||||||
|
|
||||||
// if (userRole === UserRole.WRITE) {
|
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||||
// 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'
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logbookContext.state.isDrawerOpen) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
<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-10 mt-5 mb-5'>
|
<div className='prose max-w-none col-span-6 mt-5 mb-5'>
|
||||||
<h1>Logbook</h1>
|
<h1>Logbook</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<div className='col-span-6 justify-self-end self-center'>
|
||||||
{userRole === UserRole.WRITE &&
|
{userRole === UserRole.WRITE &&
|
||||||
<Button
|
<button className='btn btn-primary'
|
||||||
color='primary'
|
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||||
onPress={() => onOpenCloseDrawer(FormMode.ADD)}
|
|
||||||
startContent={<FontAwesomeIcon icon={faAdd} />}
|
|
||||||
data-testid="pilot-add-button"
|
|
||||||
data-theme="lofi"
|
|
||||||
>
|
>
|
||||||
|
<FontAwesomeIcon icon={faAdd} />
|
||||||
Add Entry
|
Add Entry
|
||||||
</Button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{!state.isLoading && state.alert && (
|
{!state.isLoading && state.alert && (
|
||||||
<div className='col-span-12 mb-5'>
|
<div className='col-span-12 mb-5'>
|
||||||
<Alert
|
<Alert
|
||||||
|
className='mb-5'
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
color={'default'}
|
severity={state.alert.severity}
|
||||||
title={state.alert.message}
|
>
|
||||||
/>
|
{state.alert.message}
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!state.isLoading && (
|
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD && (
|
||||||
<div className='col-span-12'>
|
<div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg '>
|
||||||
{state.entries.length > 0 && screenSize !== ScreenSize.SM && (
|
<div className='overflow-x-auto mb-5'>
|
||||||
<div className='p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto shadow-small rounded-large w-full'>
|
<div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'>
|
||||||
<table className='min-w-full h-auto table-auto w-full'>
|
<label className='select select-sm select-ghost'>
|
||||||
<thead className='[&>tr]:first:rounded-lg'>
|
<span className='label'>Rows per page</span>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<select
|
||||||
<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}>
|
defaultValue={1}
|
||||||
{headerGroup.headers.map((header) => {
|
onChange={onRowsPerPageChanged}
|
||||||
return (
|
value={state.pagination?.pageSize}
|
||||||
<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`}
|
<option value={10}>10</option>
|
||||||
colSpan={header.colSpan}
|
<option value={25}>25</option>
|
||||||
key={header.id}
|
<option value={50}>50</option>
|
||||||
>
|
<option value={state.totalEntries}>All</option>
|
||||||
{header.isPlaceholder ? null : (
|
</select>
|
||||||
<div>
|
</label>
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
</div>
|
||||||
header.getContext()
|
<table className='table min-w-full h-auto table-auto w-full'>
|
||||||
)}
|
<thead className='bg-base-200'>
|
||||||
{/* {header.column.getCanFilter() ? (
|
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
||||||
<div>
|
<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}>
|
||||||
<Filter column={header.column} table={table} />
|
{headerGroup.headers.map((header, headerIndex) => {
|
||||||
</div>
|
|
||||||
) : null} */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<>
|
|
||||||
{table.getRowModel().rows.map((row) => {
|
|
||||||
return (
|
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}>
|
<th
|
||||||
{row.getVisibleCells().map((cell) => {
|
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`}
|
||||||
return (
|
colSpan={header.colSpan}
|
||||||
<td
|
key={header.id}
|
||||||
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}
|
{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(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext()
|
cell.getContext()
|
||||||
)}
|
)}
|
||||||
</td>
|
</div>
|
||||||
);
|
</td>
|
||||||
})}
|
);
|
||||||
</tr>
|
})}
|
||||||
);
|
</tr>
|
||||||
})}
|
);
|
||||||
</>
|
})}
|
||||||
</tbody>
|
</>
|
||||||
<thead className='[&>tr]:first:rounded-lg'>
|
</tbody>
|
||||||
{table.getFooterGroups().map((footerGroup, index) => {
|
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||||
if (index === 0) {
|
{table.getFooterGroups().map((footerGroup, index) => {
|
||||||
return (
|
if (index === 0) {
|
||||||
<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}>
|
return (
|
||||||
{footerGroup.headers.map((header) => {
|
<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}>
|
||||||
return (
|
{footerGroup.headers.map((header) => {
|
||||||
<td
|
return (
|
||||||
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'
|
<td
|
||||||
key={header.id}
|
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'
|
||||||
align={header.column.columnDef.meta?.align}
|
key={header.id}
|
||||||
>
|
>
|
||||||
|
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.footer,
|
header.column.columnDef.footer,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)
|
||||||
</td>
|
}
|
||||||
);
|
</div>
|
||||||
})}
|
</td>
|
||||||
</tr>
|
);
|
||||||
);
|
})}
|
||||||
}
|
</tr>
|
||||||
})}
|
);
|
||||||
</thead>
|
}
|
||||||
</table>
|
})}
|
||||||
|
</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>
|
</div>
|
||||||
)}
|
|
||||||
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
|
||||||
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} />
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* {state.isLoading && !state.alert && (
|
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<>
|
<div className='col-span-12'>
|
||||||
<div>
|
<>
|
||||||
<Loading size='xl' />
|
{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>
|
<div className='col-span-12 justify-self-center'>
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{logbookContext.state.isDrawerOpen && (
|
{logbookContext.state.isDrawerOpen && (
|
||||||
<LogbookDrawer
|
<LogbookDrawer
|
||||||
@@ -642,14 +777,6 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
title="Confirm Delete"
|
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 { FormMode } from '../../enums/formMode';
|
||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { LogbookEntry } from './LogbookEntry.interface';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
@@ -10,4 +10,7 @@ export interface LogbookState {
|
|||||||
isConfirmDialogLoading: boolean;
|
isConfirmDialogLoading: boolean;
|
||||||
isConfirmDialogOpen: boolean;
|
isConfirmDialogOpen: boolean;
|
||||||
isLoading: 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 { FormMode } from '../../enums/formMode';
|
||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { LogbookEntry } from './LogbookEntry.interface';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
@@ -7,11 +7,12 @@ import { LogbookState } from './LogbookState.interface';
|
|||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
||||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
|
| { 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_ALERT'; payload: Alert | undefined }
|
||||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
| { 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 = {
|
export const initialState: LogbookState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
@@ -19,7 +20,13 @@ export const initialState: LogbookState = {
|
|||||||
entries: [],
|
entries: [],
|
||||||
isConfirmDialogLoading: false,
|
isConfirmDialogLoading: false,
|
||||||
isConfirmDialogOpen: false,
|
isConfirmDialogOpen: false,
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
pages: [],
|
||||||
|
pagination: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
totalEntries: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
@@ -42,7 +49,8 @@ export const reducer = (
|
|||||||
case 'SET_ENTRIES': {
|
case 'SET_ENTRIES': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
entries: action.payload
|
entries: action.payload.entries,
|
||||||
|
totalEntries: action.payload.totalEntries
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_ALERT': {
|
case 'SET_ALERT': {
|
||||||
@@ -63,6 +71,18 @@ export const reducer = (
|
|||||||
isLoading: action.payload
|
isLoading: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_PAGES': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pages: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_PAGINATION': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pagination: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { Accordion, AccordionItem, Card, CardBody, CardHeader } from '@heroui/react'
|
|
||||||
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
||||||
import TrackMap from "../trackMap/TrackMap";
|
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 LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{logs.map((log) => {
|
{logs.map((log) => {
|
||||||
console.log(log)
|
const date = new Date(log.date.replace('Z', ''));
|
||||||
const date = new Date(log.date);
|
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||||
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||||
<Card className='p-4' key={log.id}>
|
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-2' : ''}`} key={log.id}>
|
||||||
<CardHeader>
|
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||||
<h2 className='font-bold text-2xl'>{formattedDate}</h2>
|
<div>
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
||||||
<div className='mb-5'>
|
<div className='mb-5'>
|
||||||
<TrackMap
|
<TrackMap
|
||||||
@@ -26,49 +27,50 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<Accordion variant='bordered'>
|
</div>
|
||||||
<AccordionItem key='1' title='Details'>
|
<div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
|
||||||
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
|
<input type='checkbox' />
|
||||||
<div className='col-span-6 font-bold'>
|
<div className='collapse-title font-semibold'>Details</div>
|
||||||
<span>Aircraft Make and Model</span>
|
<div className='collapse-content'>
|
||||||
</div>
|
<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'>
|
<div className='col-span-6 font-bold'>
|
||||||
<span>{log.aircraftMakeModel}</span>
|
<span>Aircraft Make and Model</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>
|
||||||
</AccordionItem>
|
<div className='col-span-6'>
|
||||||
</Accordion>
|
<span>{log.aircraftMakeModel}</span>
|
||||||
|
</div>
|
||||||
</CardBody>
|
<div className='col-span-6 font-bold'>
|
||||||
</Card>
|
<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>
|
</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 { LogbookDrawerProps } from "./LogbookDrawerProps.interface";
|
||||||
import LogForm from "../logForm/LogForm";
|
import LogForm from "../logForm/LogForm";
|
||||||
import TracksForm from "../tracksForm/TracksForm";
|
import TracksForm from "../tracksForm/TracksForm";
|
||||||
|
import Alert from '../alert/Alert';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faClock, faMapLocationDot, faSave, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faClock, faMapLocationDot, faSave, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
@@ -9,10 +10,11 @@ import { FormMode } from "../../enums/formMode";
|
|||||||
import httpClient from "../../httpClient/httpClient";
|
import httpClient from "../../httpClient/httpClient";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
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 LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||||
const [activeTab, setActiveTab] = useState<Key>('time');
|
const [activeTab, setActiveTab] = useState<string>('time');
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
pilotId: '',
|
pilotId: '',
|
||||||
date: null,
|
date: null,
|
||||||
@@ -40,6 +42,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
|||||||
};
|
};
|
||||||
const methods = useForm();
|
const methods = useForm();
|
||||||
const logbookContext = useLogbookContext()
|
const logbookContext = useLogbookContext()
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
methods.reset(defaultValues);
|
methods.reset(defaultValues);
|
||||||
@@ -48,8 +51,8 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: unknown) => {
|
const onSubmit = async (data: unknown) => {
|
||||||
console.log(data)
|
|
||||||
try {
|
try {
|
||||||
|
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: true });
|
||||||
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true });
|
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true });
|
||||||
|
|
||||||
if (!logbookContext.state.selectedLogId) {
|
if (!logbookContext.state.selectedLogId) {
|
||||||
@@ -59,116 +62,116 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
methods.reset(defaultValues);
|
methods.reset(defaultValues);
|
||||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
|
||||||
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
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 {
|
} finally {
|
||||||
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false });
|
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) => {
|
const onTabClicked = (event: any) => {
|
||||||
setActiveTab(key)
|
setActiveTab(event.target.ariaLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<div className='drawer drawer-end'>
|
||||||
closeButton={
|
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={logbookContext.state.isDrawerOpen} />
|
||||||
<Button isIconOnly>
|
<div className="drawer-side">
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<label
|
||||||
</Button>
|
htmlFor='my-drawer-1'
|
||||||
}
|
aria-label='close-sidebar'
|
||||||
isOpen={logbookContext.state.isDrawerOpen}
|
className='drawer-overlay'
|
||||||
onClose={onCancel}
|
></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%]'}`}>
|
||||||
<DrawerContent>
|
<FormProvider {...methods}>
|
||||||
<FormProvider {...methods}>
|
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
<div className='grid grid-cols-12 mb-6'>
|
||||||
<DrawerHeader>
|
<div className="col-span-10">
|
||||||
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
<h2 className="mt-0 mb-0 self-center">
|
||||||
</DrawerHeader>
|
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||||
<DrawerBody>
|
</h2>
|
||||||
{logbookContext.state.formAlert && (
|
</div>
|
||||||
<div className='col-span-12'>
|
<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
|
<Alert
|
||||||
|
className='mb-5'
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
color={logbookContext.state.formAlert.severity}
|
severity={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>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<TracksForm />
|
{logbookContext.state.formAlert.message}
|
||||||
</Tab>
|
</Alert>
|
||||||
}
|
)}
|
||||||
</Tabs>
|
<div className='tabs tabs-lift mb-5'>
|
||||||
</DrawerBody>
|
<label className='tab'>
|
||||||
{activeTab !== 'tracks' &&
|
<input aria-label='time' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'time' ? true : false} />
|
||||||
<DrawerFooter>
|
<FontAwesomeIcon className='mr-1'icon={faClock} />
|
||||||
<div className='grid grid-cols-12 gap-3'>
|
Time
|
||||||
<div className='col-span-12 justify-self-end self-center'>
|
</label>
|
||||||
<Button
|
<div className='tab-content border-base-300 p-6'>
|
||||||
disabled={
|
<LogForm />
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</DrawerFooter>
|
{activeTab !== 'tracks' &&
|
||||||
}
|
<div>
|
||||||
</form>
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
</FormProvider>
|
<div className='col-span-12 justify-self-end self-center'>
|
||||||
</DrawerContent>
|
<button
|
||||||
</Drawer>
|
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 { Card, CardActions, CardBody, CardHeader} from "@noahspan/noahspan-components"
|
||||||
import { PilotCardProps } from "./PilotCardProps.interface"
|
// import { PilotCardProps } from "./PilotCardProps.interface"
|
||||||
import ActionMenu from "../actionMenu/ActionMenu"
|
// import ActionMenu from "../actionMenu/ActionMenu"
|
||||||
|
|
||||||
const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
// const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
||||||
return (
|
// return (
|
||||||
<div>
|
// <div>
|
||||||
{pilots.map((pilot) => {
|
// {pilots.map((pilot) => {
|
||||||
return (
|
// return (
|
||||||
<div>
|
// <div>
|
||||||
<Card key={pilot.id}>
|
// <Card key={pilot.id}>
|
||||||
<CardBody>
|
// <CardBody>
|
||||||
<CardHeader>{pilot.name}</CardHeader>
|
// <CardHeader>{pilot.name}</CardHeader>
|
||||||
<CardActions>
|
// <CardActions>
|
||||||
<ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
// <ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
||||||
</CardActions>
|
// </CardActions>
|
||||||
</CardBody>
|
// </CardBody>
|
||||||
</Card>
|
// </Card>
|
||||||
</div>
|
// </div>
|
||||||
)
|
// )
|
||||||
})}
|
// })}
|
||||||
</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 { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||||
import { IPilotFormProps } from './IPilotFormProps';
|
import { IPilotFormProps } from './IPilotFormProps';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Person } from '@microsoft/microsoft-graph-types';
|
import { Person } from '@microsoft/microsoft-graph-types';
|
||||||
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
// import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||||
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
// import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||||
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
// import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||||
import { useOidc } from '../../auth/oidcConfig';
|
import { useOidc } from '../../auth/oidcConfig';
|
||||||
import httpClient from '../../httpClient/httpClient';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
|
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { states } from './states';
|
import { states } from './states';
|
||||||
|
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||||
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
|
|
||||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||||
pilotId,
|
pilotId,
|
||||||
@@ -44,39 +45,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
});
|
});
|
||||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||||
const [isError, setIsError] = useState<boolean>(false);
|
const [isError, setIsError] = useState<boolean>(false);
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
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 onCancel = () => {
|
const onCancel = () => {
|
||||||
methods.reset(defaultValues);
|
methods.reset(defaultValues);
|
||||||
@@ -138,245 +107,225 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}, [pilotId]);
|
}, [pilotId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<div className='drawer drawer-end'>
|
||||||
closeButton={
|
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={isDrawerOpen} />
|
||||||
<Button isIconOnly>
|
<div className='drawer-side'>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<label
|
||||||
</Button>
|
htmlFor='my-drawer-1'
|
||||||
}
|
aria-label='close-sidebar'
|
||||||
isOpen={isDrawerOpen}
|
className='drawer-overlay'
|
||||||
placement='right'
|
></label>
|
||||||
data-testid="pilot-drawer"
|
<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%]'}`}>
|
||||||
onClose={onCancel}
|
<FormProvider {...methods}>
|
||||||
size='xl'
|
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||||
>
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
<DrawerContent>
|
<div className="col-span-10">
|
||||||
<FormProvider {...methods}>
|
<h2 className="mt-0 mb-0 self-center">
|
||||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||||
<DrawerHeader>
|
</h2>
|
||||||
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
</div>
|
||||||
</DrawerHeader>
|
<div className="col-span-2 justify-self-end self-center">
|
||||||
<DrawerBody>
|
<button className="btn btn-ghost" onClick={onCancel}>
|
||||||
<div className='grid grid-cols-12 gap-3'>
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
<div className='col-span-3 self-center'>
|
</button>
|
||||||
<h6>Name *</h6>
|
</div>
|
||||||
</div>
|
<div className='col-span-3 self-center'>
|
||||||
<div className='col-span-9'>
|
<span>Name *</span>
|
||||||
<Autocomplete
|
</div>
|
||||||
inputValue={peoplePickerValue}
|
<div className='col-span-9'>
|
||||||
isLoading={isPeoplePickerLoading}
|
<Controller
|
||||||
items={peoplePickerResults}
|
name="name"
|
||||||
onInputChange={onPeoplePickerSearch}
|
control={methods.control}
|
||||||
onSelectionChange={(key: Key | null) => onPersonSelected(key as string)}
|
rules={{ required: 'A name is required' }}
|
||||||
>
|
render={({ field: { onChange, value } }) => (
|
||||||
{peoplePickerResults.map((person: Person) => (
|
<input
|
||||||
<AutocompleteItem key={person.userPrincipalName}>
|
type='text'
|
||||||
{person.displayName}
|
className='input w-full'
|
||||||
</AutocompleteItem>
|
disabled={isDisabled}
|
||||||
))}
|
onChange={onChange}
|
||||||
</Autocomplete>
|
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
|
|
||||||
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>
|
||||||
|
{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>
|
||||||
}
|
<Grid size={12}>
|
||||||
<Grid size={12}>
|
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
||||||
<PilotFormCertificates isDisabled={isDisabled} mode={mode} />
|
</Grid> */}
|
||||||
</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>
|
<div className='col-span-12 justify-self-end self-center'>
|
||||||
</DrawerBody>
|
<button
|
||||||
<DrawerFooter>
|
className='btn'
|
||||||
<Button
|
disabled={
|
||||||
disabled={
|
isDisabled && mode.toString() !== FormMode.VIEW
|
||||||
isDisabled && mode.toString() !== FormMode.VIEW
|
? isDisabled
|
||||||
? isDisabled
|
: false
|
||||||
: false
|
}
|
||||||
}
|
onClick={onCancel}
|
||||||
startContent={<FontAwesomeIcon icon={faXmark} />}
|
data-testid="pilot-cancel-button"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
Save
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</Button>
|
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||||
)}
|
</button>
|
||||||
</DrawerFooter>
|
{mode.toString() !== FormMode.VIEW && (
|
||||||
</form>
|
<button
|
||||||
</FormProvider>
|
className='btn btn-primary ml-2.5'
|
||||||
</DrawerContent>
|
disabled={isDisabled}
|
||||||
</Drawer>
|
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 { PilotFormCertificatesProps } from './PilotFormCertificatesProps.interface';
|
||||||
import {
|
// import {
|
||||||
Button,
|
// Button,
|
||||||
DatePicker,
|
// DatePicker,
|
||||||
Icon,
|
// Icon,
|
||||||
IconButton,
|
// IconButton,
|
||||||
IconName,
|
// IconName,
|
||||||
Input,
|
// Input,
|
||||||
Select
|
// Select
|
||||||
} from '@noahspan/noahspan-components';
|
// } from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { FormMode } from '../../enums/formMode';
|
// import { FormMode } from '../../enums/formMode';
|
||||||
|
|
||||||
const PilotFormCertificates = ({
|
// const PilotFormCertificates = ({
|
||||||
isDisabled,
|
// isDisabled,
|
||||||
mode
|
// mode
|
||||||
}: PilotFormCertificatesProps ) => {
|
// }: PilotFormCertificatesProps ) => {
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
formState: { errors },
|
// formState: { errors },
|
||||||
} = useFormContext();
|
// } = useFormContext();
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
// const { fields, append, remove } = useFieldArray({
|
||||||
name: 'certificates',
|
// name: 'certificates',
|
||||||
control
|
// control
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div>
|
// <div>
|
||||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
// {fields.length > 0 || mode !== FormMode.VIEW &&
|
||||||
<div>
|
// <div>
|
||||||
<h5>Certificates</h5>
|
// <h5>Certificates</h5>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
{fields.length > 0 && (
|
// {fields.length > 0 && (
|
||||||
<>
|
// <>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Type</h6>
|
// <h6>Type</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Number</h6>
|
// <h6>Number</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Date of Issue</h6>
|
// <h6>Date of Issue</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
</div>
|
// </div>
|
||||||
{fields.map((field, index) => {
|
// {fields.map((field, index) => {
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name={`certificates.${index}.type`}
|
// name={`certificates.${index}.type`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<Select
|
// <Select
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
options={
|
// options={
|
||||||
[
|
// [
|
||||||
{
|
// {
|
||||||
label: 'Student',
|
// label: 'Student',
|
||||||
value: 'student'
|
// value: 'student'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Private',
|
// label: 'Private',
|
||||||
value: 'private'
|
// value: 'private'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Instrument',
|
// label: 'Instrument',
|
||||||
value: 'instrument'
|
// value: 'instrument'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Recreational',
|
// label: 'Recreational',
|
||||||
value: 'recreational'
|
// value: 'recreational'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Sport',
|
// label: 'Sport',
|
||||||
value: 'sport'
|
// value: 'sport'
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
value={value}
|
// value={value}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name={`certificates.${index}.number`}
|
// name={`certificates.${index}.number`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<Input
|
// <Input
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
value={value}
|
// value={value}
|
||||||
/>
|
// />
|
||||||
)
|
// )
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name={`certificates.${index}.dateOfIssue`}
|
// name={`certificates.${index}.dateOfIssue`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<DatePicker
|
// <DatePicker
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
value={value}
|
// value={value}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<IconButton
|
// <IconButton
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onClick={() => remove(index)}
|
// onClick={() => remove(index)}
|
||||||
>
|
// >
|
||||||
<Icon iconName={IconName.TRASH} size='sm' />
|
// <Icon iconName={IconName.TRASH} size='sm' />
|
||||||
</IconButton>
|
// </IconButton>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
})}
|
// })}
|
||||||
</>
|
// </>
|
||||||
)}
|
// )}
|
||||||
{!isDisabled &&
|
// {!isDisabled &&
|
||||||
<div>
|
// <div>
|
||||||
<Button
|
// <Button
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
append({
|
// append({
|
||||||
type: '',
|
// type: '',
|
||||||
number: '',
|
// number: '',
|
||||||
dateOfIssue: null
|
// dateOfIssue: null
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
startContent={<Icon iconName={IconName.PLUS} />}
|
// startContent={<Icon iconName={IconName.PLUS} />}
|
||||||
>
|
// >
|
||||||
Add Certificate
|
// Add Certificate
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
export default PilotFormCertificates;
|
// export default PilotFormCertificates;
|
||||||
|
|||||||
@@ -1,129 +1,129 @@
|
|||||||
import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interface';
|
// import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interface';
|
||||||
import {
|
// import {
|
||||||
Button,
|
// Button,
|
||||||
DatePicker,
|
// DatePicker,
|
||||||
Icon,
|
// Icon,
|
||||||
IconButton,
|
// IconButton,
|
||||||
IconName,
|
// IconName,
|
||||||
Select
|
// Select
|
||||||
} from '@noahspan/noahspan-components';
|
// } from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { FormMode } from '../../enums/formMode';
|
// import { FormMode } from '../../enums/formMode';
|
||||||
|
|
||||||
const PilotFormEndorsements = ({
|
// const PilotFormEndorsements = ({
|
||||||
mode,
|
// mode,
|
||||||
isDisabled
|
// isDisabled
|
||||||
}: PilotFormEndorsementsProps) => {
|
// }: PilotFormEndorsementsProps) => {
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
formState: { errors },
|
// formState: { errors },
|
||||||
setValue
|
// setValue
|
||||||
} = useFormContext();
|
// } = useFormContext();
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
// const { fields, append, remove } = useFieldArray({
|
||||||
name: 'endorsements',
|
// name: 'endorsements',
|
||||||
control
|
// control
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div>
|
// <div>
|
||||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
// {fields.length > 0 || mode !== FormMode.VIEW &&
|
||||||
<div>
|
// <div>
|
||||||
<h5>Endorsements</h5>
|
// <h5>Endorsements</h5>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
{fields.length > 0 && (
|
// {fields.length > 0 && (
|
||||||
<>
|
// <>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Type</h6>
|
// <h6>Type</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Date of Issue</h6>
|
// <h6>Date of Issue</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div></div>
|
// <div></div>
|
||||||
{fields.map((field, index) => {
|
// {fields.map((field, index) => {
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name={`endorsements.${index}.type`}
|
// name={`endorsements.${index}.type`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<Select
|
// <Select
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
options={
|
// options={
|
||||||
[
|
// [
|
||||||
{
|
// {
|
||||||
label: 'Complex',
|
// label: 'Complex',
|
||||||
value: 'complex'
|
// value: 'complex'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'High Performance',
|
// label: 'High Performance',
|
||||||
value: 'highPerfomance'
|
// value: 'highPerfomance'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'High Altitude',
|
// label: 'High Altitude',
|
||||||
value: 'highAltitude'
|
// value: 'highAltitude'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Tailwheel',
|
// label: 'Tailwheel',
|
||||||
value: 'tailwheel'
|
// value: 'tailwheel'
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
value={value ? value : ''}
|
// value={value ? value : ''}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name={`endorsements.${index}.dateOfIssue`}
|
// name={`endorsements.${index}.dateOfIssue`}
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<DatePicker
|
// <DatePicker
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
value={value}
|
// value={value}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<IconButton
|
// <IconButton
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onClick={() => remove(index)}
|
// onClick={() => remove(index)}
|
||||||
>
|
// >
|
||||||
<Icon iconName={IconName.TRASH} size="sm" />
|
// <Icon iconName={IconName.TRASH} size="sm" />
|
||||||
</IconButton>
|
// </IconButton>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
})}
|
// })}
|
||||||
</>
|
// </>
|
||||||
)}
|
// )}
|
||||||
{!isDisabled &&
|
// {!isDisabled &&
|
||||||
<div>
|
// <div>
|
||||||
<Button
|
// <Button
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
append({
|
// append({
|
||||||
type: '',
|
// type: '',
|
||||||
dateOfIssue: null
|
// dateOfIssue: null
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
startContent={<Icon iconName={IconName.PLUS} />}
|
// startContent={<Icon iconName={IconName.PLUS} />}
|
||||||
>
|
// >
|
||||||
Add Endorsement
|
// Add Endorsement
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
export default PilotFormEndorsements;
|
// export default PilotFormEndorsements;
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
import { DatePicker, Select } from '@noahspan/noahspan-components';
|
// import { DatePicker, Select } from '@noahspan/noahspan-components';
|
||||||
import { Controller, useFormContext } from "react-hook-form"
|
// import { Controller, useFormContext } from "react-hook-form"
|
||||||
import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
// import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
||||||
|
|
||||||
const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
// const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
formState: { errors },
|
// formState: { errors },
|
||||||
setValue
|
// setValue
|
||||||
} = useFormContext();
|
// } = useFormContext();
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div>
|
// <div>
|
||||||
<div>
|
// <div>
|
||||||
<h5>Medical</h5>
|
// <h5>Medical</h5>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Class</h6>
|
// <h6>Class</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name="medicalClass"
|
// name="medicalClass"
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<Select
|
// <Select
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
options={
|
// options={
|
||||||
[
|
// [
|
||||||
{
|
// {
|
||||||
label: 'First',
|
// label: 'First',
|
||||||
value: 'first'
|
// value: 'first'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Second',
|
// label: 'Second',
|
||||||
value: 'second'
|
// value: 'second'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Third',
|
// label: 'Third',
|
||||||
value: 'third'
|
// value: 'third'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Basic Med',
|
// label: 'Basic Med',
|
||||||
value: 'basicMed'
|
// value: 'basicMed'
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
value={value ? value : ''}
|
// value={value ? value : ''}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<h6>Expiration</h6>
|
// <h6>Expiration</h6>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<Controller
|
// <Controller
|
||||||
name="medicalExpiration"
|
// name="medicalExpiration"
|
||||||
control={control}
|
// control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
// render={({ field: { onChange, value } }) => {
|
||||||
return (
|
// return (
|
||||||
<DatePicker
|
// <DatePicker
|
||||||
disabled={isDisabled}
|
// disabled={isDisabled}
|
||||||
onChange={onChange}
|
// onChange={onChange}
|
||||||
value={value}
|
// value={value}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
export default PilotFormMedical
|
// export default PilotFormMedical
|
||||||
@@ -4,20 +4,44 @@ import { AxiosError, AxiosResponse } from 'axios';
|
|||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||||
import PilotCard from '../pilotCard/PilotCard';
|
// import PilotCard from '../pilotCard/PilotCard';
|
||||||
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||||
import { UserRole } from '../../enums/userRole';
|
import { UserRole } from '../../enums/userRole';
|
||||||
import httpClient from '../../httpClient/httpClient'
|
import httpClient from '../../httpClient/httpClient'
|
||||||
import { ScreenSize } from '../../enums/screenSize';
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
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 { 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 Pilots: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const { userRole } = useUserRole();
|
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 () => {
|
const getPilots = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -34,7 +58,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
} else {
|
} 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: [] });
|
dispatch({ type: 'SET_PILOTS', payload: [] });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,7 +66,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
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 {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
||||||
@@ -50,7 +74,6 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
|
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
|
||||||
console.log(pilotId)
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case FormMode.ADD:
|
case FormMode.ADD:
|
||||||
case FormMode.EDIT:
|
case FormMode.EDIT:
|
||||||
@@ -102,7 +125,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
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 {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
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',
|
id: 'name',
|
||||||
name: 'Name'
|
accessorKey: 'name',
|
||||||
|
header: 'Name'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
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 textAlignment = {
|
||||||
const cellValue = pilot[columnKey]
|
center: 'text-center',
|
||||||
|
left: 'text-start',
|
||||||
|
right: 'text-end'
|
||||||
|
};
|
||||||
|
|
||||||
switch (columnKey) {
|
const table = useReactTable({
|
||||||
case 'actions': {
|
data: state.pilots,
|
||||||
return (
|
columns: columns,
|
||||||
<Dropdown>
|
getCoreRowModel: getCoreRowModel(),
|
||||||
<DropdownTrigger>
|
getPaginationRowModel: getPaginationRowModel()
|
||||||
<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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isFormOpen) {
|
if (!state.isFormOpen) {
|
||||||
@@ -183,63 +181,127 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
<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-10 mt-5 mb-5' >
|
<div className='prose max-w-none col-span-6 mt-5 mb-5' >
|
||||||
<h1>Pilots</h1>
|
<h1>Pilots</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<div className='col-span-6 justify-self-end self-center'>
|
||||||
{userRole === UserRole.WRITE &&
|
{!state.isLoading && userRole === UserRole.WRITE &&
|
||||||
<Button
|
<button
|
||||||
color='primary'
|
className='btn btn-primary'
|
||||||
onPress={() => onOpenClosePilotForm(FormMode.ADD)}
|
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||||
startContent={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
data-testid="pilot-add-button"
|
data-testid="pilot-add-button"
|
||||||
>
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
Add Pilot
|
Add Pilot
|
||||||
</Button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{!state.isLoading && state.alert && (
|
{!state.isLoading && state.alert && (
|
||||||
<div className='col-span-12'>
|
<div className='col-span-12'>
|
||||||
<Alert
|
<Alert
|
||||||
|
className='mb-5'
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
color={state.alert.severity}
|
severity={state.alert.severity}
|
||||||
title={state.alert.message}
|
>
|
||||||
/>
|
{state.alert.message}
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='col-span-12'>
|
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
|
||||||
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
|
<div className='col-span-12 bg-base-100 p-5 border border-base-100 rounded-lg'>
|
||||||
<Table>
|
<table className='table min-w-full h-auto table-auto w-full'>
|
||||||
<TableHeader columns={columns}>
|
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||||
{(column) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableColumn
|
<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}>
|
||||||
key={column.id}
|
{headerGroup.headers.map((header) => {
|
||||||
align={column.id === "actions" ? "center" : "start"}
|
return (
|
||||||
>
|
<th
|
||||||
{column.name}
|
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`}
|
||||||
</TableColumn>
|
colSpan={header.colSpan}
|
||||||
)}
|
key={header.id}
|
||||||
</TableHeader>
|
>
|
||||||
<TableBody items={state.pilots}>
|
{header.isPlaceholder ? null : (
|
||||||
{(item) => (
|
<div>
|
||||||
<TableRow key={item.id}>
|
{flexRender(
|
||||||
{(columnKey => (
|
header.column.columnDef.header,
|
||||||
<TableCell>
|
header.getContext()
|
||||||
{renderCell(item, columnKey)}
|
)}
|
||||||
</TableCell>
|
{/* {header.column.getCanFilter() ? (
|
||||||
))}
|
<div>
|
||||||
</TableRow>
|
<Filter column={header.column} table={table} />
|
||||||
)}
|
</div>
|
||||||
</TableBody>
|
) : null} */}
|
||||||
</Table>
|
</div>
|
||||||
}
|
)}
|
||||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
</th>
|
||||||
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
|
);
|
||||||
}
|
})}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{state.isFormOpen && (
|
{state.isFormOpen && (
|
||||||
<PilotForm
|
<PilotForm
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
|||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { User } from '@microsoft/microsoft-graph-types';
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
import { useOidc } from '../../auth/oidcConfig';
|
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 httpClient from '../../httpClient/httpClient'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
import { faBars, faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
|
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||||
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
|
|
||||||
const SiteNav = () => {
|
const SiteNav = () => {
|
||||||
const [userPhoto, setUserPhoto] = useState<string>();
|
const [userPhoto, setUserPhoto] = useState<string>();
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
const { isUserLoggedIn, logout, login } = useOidc()
|
const { isUserLoggedIn, logout, login } = useOidc()
|
||||||
const { pathname } = useLocation()
|
|
||||||
const pages = [
|
const pages = [
|
||||||
{
|
{
|
||||||
name: 'Flights',
|
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(() => {
|
useEffect(() => {
|
||||||
const setUserProfile = async () => {
|
const setUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const userProfile = await getUserProfile();
|
const userProfile = await getUserProfile();
|
||||||
const userPhoto = await getUserPhoto();
|
// const userPhoto = await getUserPhoto();
|
||||||
|
|
||||||
setUserPhoto(userPhoto);
|
// setUserPhoto(userPhoto);
|
||||||
|
|
||||||
appContext.dispatch({
|
appContext.dispatch({
|
||||||
type: 'SET_USER_PROFILE',
|
type: 'SET_USER_PROFILE',
|
||||||
@@ -79,53 +106,62 @@ const SiteNav = () => {
|
|||||||
}, [isUserLoggedIn]);
|
}, [isUserLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar isBordered maxWidth='full' position='static'>
|
<div className="navbar bg-base-100 shadow-sm w-full">
|
||||||
<NavbarContent>
|
<div className={`navbar-start ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
|
||||||
<NavbarBrand>
|
{screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
|
||||||
<img
|
<div className="dropdown">
|
||||||
height={35}
|
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
|
||||||
width={35}
|
<FontAwesomeIcon icon={faBars} size='xl' />
|
||||||
src='noahspan-logo.png'
|
</div>
|
||||||
style={{ marginRight: '5px' }}
|
<ul
|
||||||
/>
|
tabIndex={-1}
|
||||||
<FontAwesomeIcon icon={faPlane} size='2x' />
|
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||||
</NavbarBrand>
|
<Links />
|
||||||
</NavbarContent>
|
</ul>
|
||||||
<NavbarContent justify='center'>
|
</div>
|
||||||
{pages.length > 0 && pages.map((page, index) => {
|
) : (
|
||||||
return (
|
<Brand />
|
||||||
<NavbarItem isActive={pathname === page.path ? true : false} key={index}>
|
)}
|
||||||
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}>
|
</div>
|
||||||
{page.name}
|
<div className="navbar-center">
|
||||||
</Link>
|
<ul className="menu menu-horizontal px-1">
|
||||||
</NavbarItem>
|
{screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
|
||||||
)
|
<Brand />
|
||||||
})}
|
) : (
|
||||||
</NavbarContent>
|
<Links />
|
||||||
<NavbarContent justify='end'>
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className={`navbar-end ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'mr-8' : ''}`}>
|
||||||
{!isUserLoggedIn &&
|
{!isUserLoggedIn &&
|
||||||
<Button
|
<button className='btn btn-ghost' onClick={() => login()}><FontAwesomeIcon icon={faSignIn} />Sign In</button>
|
||||||
color='default'
|
|
||||||
onPress={() => login()}
|
|
||||||
startContent={<FontAwesomeIcon icon={faSignIn} />}
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
{isUserLoggedIn &&
|
{isUserLoggedIn &&
|
||||||
<Dropdown>
|
<div className='dropdown dropdown-end'>
|
||||||
<DropdownTrigger>
|
<div tabIndex={0} role='button'>
|
||||||
<Avatar name={appContext.state.userProfile.displayName?.toString()} src={userPhoto}></Avatar>
|
<div className={`avatar ${userPhoto ? userPhoto : 'avatar-placeholder'}`}>
|
||||||
</DropdownTrigger>
|
{userPhoto &&
|
||||||
<DropdownMenu>
|
<div className='w-12 rounded-full'>
|
||||||
<DropdownItem key='signout' onPress={() => logout({redirectTo: 'specific url', url: '/'})} startContent={<FontAwesomeIcon icon={faSignOut} />}>
|
<img src={userPhoto} />
|
||||||
Sign Out
|
</div>
|
||||||
</DropdownItem>
|
}
|
||||||
</DropdownMenu>
|
{!userPhoto &&
|
||||||
</Dropdown>
|
<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>
|
</div>
|
||||||
</Navbar>
|
</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 { useAuth } from 'react-oidc-context'
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||||
import ReactLeafletKml from 'react-leaflet-kml';
|
import ReactLeafletKml from 'react-leaflet-kml';
|
||||||
import 'swiper/css';
|
|
||||||
import 'swiper/css/pagination';
|
|
||||||
import 'swiper/css';
|
|
||||||
import './TrackMap.css';
|
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import httpClient from '../../httpClient/httpClient'
|
import httpClient from '../../httpClient/httpClient'
|
||||||
|
|
||||||
@@ -41,7 +37,7 @@ const TrackMap = ({ height, logId, tracks }: TrackMapProps) => {
|
|||||||
center={[45.14489, -93.21019]}
|
center={[45.14489, -93.21019]}
|
||||||
scrollWheelZoom={false}
|
scrollWheelZoom={false}
|
||||||
style={{ height: height, width: '100%' }}
|
style={{ height: height, width: '100%' }}
|
||||||
zoom={8}
|
zoom={7}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
|||||||
import { initialState, reducer } from "./reducer";
|
import { initialState, reducer } from "./reducer";
|
||||||
import TrackMap from "../trackMap/TrackMap";
|
import TrackMap from "../trackMap/TrackMap";
|
||||||
import httpClient from "../../httpClient/httpClient";
|
import httpClient from "../../httpClient/httpClient";
|
||||||
import { Button, Input, Spinner } from '@heroui/react';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
|
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||||
@@ -28,7 +27,7 @@ const TracksForm = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
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])
|
}, [logbookContext.state.selectedLogId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(logbookContext.state.formMode)
|
|
||||||
if (logbookContext.state.formMode === FormMode.VIEW) {
|
if (logbookContext.state.formMode === FormMode.VIEW) {
|
||||||
dispatch({ type: 'SET_IS_DISABLED', payload: true });
|
dispatch({ type: 'SET_IS_DISABLED', payload: true });
|
||||||
}
|
}
|
||||||
}, [logbookContext.state.formMode]);
|
}, [logbookContext.state.formMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(state.isDisabled)
|
|
||||||
}, [state.isDisabled])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid grid-cols-12 gap-3'>
|
<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) => {
|
{state.tracks.length > 0 && state.tracks.map((track, index) => {
|
||||||
const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
|
const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
|
||||||
@@ -125,26 +114,25 @@ const TracksForm = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='col-span-10'>
|
<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>
|
||||||
<div className='col-span-2'>
|
<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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className='col-span-12'>
|
{logbookContext.state.formMode === FormMode.EDIT &&
|
||||||
<Button
|
<div className='col-span-12'>
|
||||||
as='label'
|
<label
|
||||||
color='primary'
|
className='btn cursor-pointer w-full'
|
||||||
isDisabled={state.isDisabled}
|
>
|
||||||
fullWidth={true}
|
<FontAwesomeIcon icon={faUpload} />
|
||||||
startContent={<FontAwesomeIcon icon={faUpload} />}
|
Upload Track
|
||||||
>
|
<input className='hidden' id='track-upload' onChange={handleFileUpload} type='file' />
|
||||||
Upload Track
|
</label>
|
||||||
<input hidden onChange={handleFileUpload} type='file' />
|
</div>
|
||||||
</Button>
|
}
|
||||||
</div>
|
|
||||||
{state.isConfirmDialogOpen && (
|
{state.isConfirmDialogOpen && (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
contentText="Are you sure you want to delete this track?"
|
contentText="Are you sure you want to delete this track?"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export enum ScreenSize {
|
export enum ScreenSize {
|
||||||
SM,
|
SM = 'SM',
|
||||||
MD,
|
MD = 'MD',
|
||||||
LG,
|
LG = 'LG',
|
||||||
XL,
|
XL = 'XL',
|
||||||
XXL
|
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) {
|
switch (true) {
|
||||||
case width < 640: {
|
case width < 640: {
|
||||||
size = ScreenSize.SM;
|
size = ScreenSize.SM;
|
||||||
console.log('small')
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case width >= 640: {
|
case width >= 640 && width < 1024: {
|
||||||
size = ScreenSize.MD;
|
size = ScreenSize.MD;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case width >= 1024: {
|
case width >= 1024 && width < 1280: {
|
||||||
size = ScreenSize.LG
|
size = ScreenSize.LG
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case width >= 1280: {
|
case width >= 1280 && width < 1536: {
|
||||||
size = ScreenSize.XL;
|
size = ScreenSize.XL;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -42,14 +42,10 @@ export const useBreakpoints = () => {
|
|||||||
const onWindowResize = () => {
|
const onWindowResize = () => {
|
||||||
const width: number = window.innerWidth;
|
const width: number = window.innerWidth;
|
||||||
const newScreenSize: ScreenSize = getWindowSize(width);
|
const newScreenSize: ScreenSize = getWindowSize(width);
|
||||||
console.log(newScreenSize)
|
|
||||||
setScreenSize(newScreenSize);
|
setScreenSize(newScreenSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(windowWidth)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onWindowResize()
|
onWindowResize()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export const useUserRole = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isUserLoggedIn && decodedIdToken) {
|
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;
|
let newUserRole: string | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Flying</title>
|
<title>Flying</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="background-color: #f5f5f5;">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface Alert {
|
export interface Alert {
|
||||||
severity: 'danger' | 'default' | 'success' | 'warning';
|
severity: 'info' | 'error' | 'success' | 'warning';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
|||||||
import LogbookContextProvider from './context/logbookContext/LogbookContextProvider.tsx'
|
import LogbookContextProvider from './context/logbookContext/LogbookContextProvider.tsx'
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { OidcProvider } from './auth/oidcConfig.ts';
|
import { OidcProvider } from './auth/oidcConfig.ts';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<OidcProvider>
|
<OidcProvider>
|
||||||
|
|||||||
@@ -1,12 +1,42 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin './hero.ts';
|
@plugin "daisyui";
|
||||||
/* Note: You may need to change the path to fit your project structure */
|
@plugin "daisyui/theme" {
|
||||||
@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
name: "lofi";
|
||||||
@custom-variant dark (&:is(.dark *));
|
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";
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@theme inline {
|
body {
|
||||||
--color-primary: #000000;
|
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 */
|
/* eslint-disable */
|
||||||
declare module '@tanstack/react-table' {
|
declare module '@tanstack/react-table' {
|
||||||
interface ColumnMeta<TData extends RowData, TValue> {
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'text-left' | 'text-center' | 'text-right';
|
||||||
headerAlign?: 'left' | 'center' | 'right';
|
className?: string;
|
||||||
|
headerAlign?: 'text-left' | 'text-center' | 'text-right';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-enable */
|
/* 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" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_URL: string;
|
readonly VITE_BASE_URL: string;
|
||||||
readonly VITE_CLIENT_ID: string;
|
readonly VITE_CLIENT_ID: string;
|
||||||
|
readonly VITE_ISSUER_URI: string;
|
||||||
readonly VITE_TENANT_ID: string;
|
readonly VITE_TENANT_ID: string;
|
||||||
readonly VITE_REDIRECT_URL: string;
|
readonly VITE_USERINFO_ENDPOINT: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
|||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
dbs:
|
dbs:
|
||||||
- path: /var/lib/data/flying.db
|
- path: /mnt/data/flying.db
|
||||||
replicas:
|
replicas:
|
||||||
- path: /mnt/backup/flying.db
|
- path: /mnt/data/backup/flying.db
|
||||||
@@ -16,11 +16,11 @@ services:
|
|||||||
container_name: restore
|
container_name: restore
|
||||||
image: litestream/litestream:0.3.13
|
image: litestream/litestream:0.3.13
|
||||||
volumes:
|
volumes:
|
||||||
- ./database/flying.db:/var/lib/data/flying.db
|
- ./database/flying.db:/mnt/data/flying.db
|
||||||
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
||||||
- backup:/mnt/data/backup
|
- backup:/mnt/data/backup
|
||||||
- data:/var/lib/data
|
- data:/mnt/data
|
||||||
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /var/lib/data/flying.db
|
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /mnt/data/flying.db
|
||||||
|
|
||||||
app:
|
app:
|
||||||
container_name: flying
|
container_name: flying
|
||||||
@@ -31,9 +31,9 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ./api/.env
|
- ./api/.env
|
||||||
environment:
|
environment:
|
||||||
- DB_PATH=../../var/lib/data/flying.db
|
- DB_PATH=../../mnt/data/flying.db
|
||||||
volumes:
|
volumes:
|
||||||
- data:/var/lib/data
|
- data:/mnt/data
|
||||||
depends_on:
|
depends_on:
|
||||||
restore:
|
restore:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
@@ -44,7 +44,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
||||||
- backup:/mnt
|
- backup:/mnt
|
||||||
- data:/var/lib/data
|
- data:/mnt/data
|
||||||
command: replicate -config /mnt/litestream/litestream.yml
|
command: replicate -config /mnt/litestream/litestream.yml
|
||||||
depends_on:
|
depends_on:
|
||||||
app:
|
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".
|
# This file is maintained automatically by "terraform init".
|
||||||
# Manual edits may be lost in future updates.
|
# 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" {
|
provider "registry.terraform.io/hashicorp/azurerm" {
|
||||||
version = "4.40.0"
|
version = "4.40.0"
|
||||||
constraints = "4.40.0"
|
constraints = "4.40.0"
|
||||||
@@ -20,3 +39,22 @@ provider "registry.terraform.io/hashicorp/azurerm" {
|
|||||||
"zh:f6fd85bfca8516f9f3840acc3f0e5e335f6195b66231c2db05c0fb43dd8aebe1",
|
"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"
|
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 = {
|
container_image = {
|
||||||
test = "noahspan/flying-app:v2.0.0-alpha"
|
test = "noahspan/flying-app:v2.0.0-alpha"
|
||||||
prod = "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]
|
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" {
|
output "container_image" {
|
||||||
value = local.container_image[var.environment]
|
value = local.container_image[var.environment]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
max_replicas = 2
|
max_replicas = 2
|
||||||
|
|
||||||
init_container {
|
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
|
cpu = 0.25
|
||||||
image = "litestream/litestream:0.5.2"
|
image = "litestream/litestream:0.5.2"
|
||||||
memory = "0.5Gi"
|
memory = "0.5Gi"
|
||||||
@@ -17,12 +17,12 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "data"
|
name = "data"
|
||||||
path = "/var/lib/data"
|
path = "/mnt/data"
|
||||||
}
|
}
|
||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "backup"
|
name = "backup"
|
||||||
path = "/mnt/data"
|
path = "/mnt/data/backup"
|
||||||
sub_path = "data"
|
sub_path = "data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,12 +42,12 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "data"
|
name = "data"
|
||||||
path = "/var/lib/data"
|
path = "/mnt/data"
|
||||||
}
|
}
|
||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "backup"
|
name = "backup"
|
||||||
path = "/mnt/data"
|
path = "/mnt/data/backup"
|
||||||
sub_path = "data"
|
sub_path = "data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
container {
|
container {
|
||||||
cpu = 0.25
|
cpu = 0.25
|
||||||
image = "noahspan/flying-app:v2.0.0-alpha"
|
image = "noahspan/flying:20589367895"
|
||||||
memory = "0.5Gi"
|
memory = "0.5Gi"
|
||||||
name = "flying"
|
name = "flying"
|
||||||
|
|
||||||
@@ -69,9 +69,19 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
secret_name = "azure-storage-connection-string"
|
secret_name = "azure-storage-connection-string"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "AUTHORITY"
|
||||||
|
value = var.AUTHORITY
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "AUDIENCE"
|
||||||
|
value = var.CLIENT_ID
|
||||||
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
name = "CLIENT_ID"
|
name = "CLIENT_ID"
|
||||||
secret_name = "client-id"
|
value = var.CLIENT_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
@@ -79,25 +89,45 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
secret_name = "client-secret"
|
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 {
|
env {
|
||||||
name = "TENANT_ID"
|
name = "TENANT_ID"
|
||||||
value = var.TENANT_ID
|
value = var.EXTERNAL_TENANT_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
name = "DB_PATH"
|
name = "DB_PATH"
|
||||||
value = "/var/lib/data/flying.db"
|
value = "/mnt/data/flying.db"
|
||||||
}
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
name = "DB_SYNC"
|
name = "DB_SYNC"
|
||||||
value = "false"
|
value = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
startup_probe {
|
startup_probe {
|
||||||
failure_count_threshold = 10
|
failure_count_threshold = 3
|
||||||
initial_delay = 1
|
initial_delay = 15
|
||||||
interval_seconds = 2
|
interval_seconds = 30
|
||||||
path = "/api/health"
|
path = "/api/health"
|
||||||
port = 3000
|
port = 3000
|
||||||
transport = "HTTP"
|
transport = "HTTP"
|
||||||
@@ -105,7 +135,7 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "data"
|
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
|
value = azurerm_storage_account.storage_account.primary_connection_string
|
||||||
}
|
}
|
||||||
|
|
||||||
secret {
|
|
||||||
name = "client-id"
|
|
||||||
value = var.CLIENT_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
secret {
|
secret {
|
||||||
name = "client-secret"
|
name = "client-secret"
|
||||||
value = var.CLIENT_SECRET
|
value = var.CLIENT_SECRET
|
||||||
@@ -159,6 +184,11 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
value = var.DOCKER_IO_PASSWORD
|
value = var.DOCKER_IO_PASSWORD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secret {
|
||||||
|
name = "session-secret"
|
||||||
|
value = var.SESSION_SECRET
|
||||||
|
}
|
||||||
|
|
||||||
lifecycle {
|
lifecycle {
|
||||||
ignore_changes = [ template[0].container[0].image, template[0].container[0].image, template[0].init_container[0].image, registry[0].server ]
|
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
|
account_name = azurerm_storage_account.storage_account.name
|
||||||
share_name = azurerm_storage_share.storage_share[0].name
|
share_name = azurerm_storage_share.storage_share[0].name
|
||||||
access_key = azurerm_storage_account.storage_account.primary_access_key
|
access_key = azurerm_storage_account.storage_account.primary_access_key
|
||||||
access_mode = "ReadOnly"
|
access_mode = "ReadWrite"
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
provider "azurerm" {
|
provider "azurerm" {
|
||||||
features {}
|
features {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provider "random" {}
|
||||||
@@ -6,6 +6,14 @@ terraform {
|
|||||||
source = "hashicorp/azurerm"
|
source = "hashicorp/azurerm"
|
||||||
version = "4.40.0"
|
version = "4.40.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
azuread = {
|
||||||
|
source = "hashicorp/azuread"
|
||||||
|
}
|
||||||
|
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backend "remote" {
|
backend "remote" {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
variable "AUTHORITY" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
variable "CLIENT_ID" {
|
variable "CLIENT_ID" {
|
||||||
type = string
|
type = string
|
||||||
@@ -17,10 +20,27 @@ variable "DOCKER_IO_USERNAME" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "ISSUER_URL" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "JWKS_URI" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
variable "RESOURCE_GROUP_NAME" {
|
variable "RESOURCE_GROUP_NAME" {
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "EXTERNAL_TENANT_ID" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "SESSION_SECRET" {
|
||||||
|
sensitive = true
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
variable "TENANT_ID" {
|
variable "TENANT_ID" {
|
||||||
type = string
|
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