Compare commits
2 Commits
126-all-lo
...
101-upgrad
| Author | SHA1 | Date | |
|---|---|---|---|
| 20b910b015 | |||
| 8d166983f1 |
@@ -10,7 +10,7 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: ${{ inputs.environment_name }}
|
environment: ${{ inputs.environment_name }}
|
||||||
steps:
|
steps:
|
||||||
@@ -38,19 +38,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm run build -w api
|
npm run build -w api
|
||||||
|
|
||||||
- name: Test API
|
|
||||||
run: |
|
|
||||||
npm run test -w api
|
|
||||||
|
|
||||||
- name: Build Client
|
- 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: |
|
run: |
|
||||||
printenv
|
|
||||||
npm run build -w client
|
npm run build -w client
|
||||||
|
|
||||||
- name: Log into Docker Hub
|
- name: Log into Docker Hub
|
||||||
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.api == 'true' || needs.changes.outputs.client == 'true' }}
|
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true' }}
|
||||||
name: build
|
name: build
|
||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
uses: ./.github/workflows/build_and_test.yaml
|
uses: ./.github/workflows/build.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_and_test.yaml
|
uses: ./.github/workflows/build.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_and_test.yaml
|
uses: ./.github/workflows/build.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: prod
|
environment_name: prod
|
||||||
version_number: ${{ github.ref_name }}
|
version_number: ${{ github.ref_name }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22
|
FROM --platform=linux/amd64 node:22-slim
|
||||||
|
|
||||||
WORKDIR app
|
WORKDIR app
|
||||||
COPY ./api/dist ./api/dist
|
COPY ./api/dist ./api/dist
|
||||||
@@ -9,8 +9,10 @@ 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
|
||||||
|
|
||||||
CMD ["node", "./app/api/dist/main.js"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||||
3
api/entrypoint.sh
Normal file
3
api/entrypoint.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/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.1.3",
|
"version": "2.0.0-alpha-3",
|
||||||
"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.11",
|
"@noahspan/noahspan-modules": "^1.2.9",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import { join } from 'path';
|
|||||||
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')
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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,8 +11,7 @@ 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 '../pilot/pilot.entity';
|
import { PilotEntity } from 'src/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, logId: string, fileName: string): Promise<string> {
|
async downloadFile(containerName: string, rowKey: string, fileName: string): Promise<string> {
|
||||||
this.containerName = containerName;
|
this.containerName = containerName;
|
||||||
|
|
||||||
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
const blockBlobClient = await this.getBlobClient(`${rowKey}/${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,25 +1,42 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { HealthController } from "./health.controller"
|
import { HealthController } from './health.controller';
|
||||||
import { HttpStatus } from "@nestjs/common";
|
import { HealthService } from './health.service';
|
||||||
|
|
||||||
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('should be defined', () => {
|
it('isHealthy => should return true', () => {
|
||||||
expect(controller).toBeDefined();
|
expect(controller).toBeDefined();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
it('should return database connected', async () => {
|
||||||
|
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(true);
|
||||||
|
|
||||||
it('isHealth => should return status ok', async () => {
|
|
||||||
const result = await controller.isHealthy();
|
const result = await controller.isHealthy();
|
||||||
|
|
||||||
expect(result).toEqual(HttpStatus.OK);
|
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isHealthy => should return error', async () => {
|
||||||
|
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = await controller.isHealthy();
|
||||||
|
|
||||||
|
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(false);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpStatus,
|
HttpException,
|
||||||
} 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<HttpStatus> {
|
async isHealthy(): Promise<boolean> {
|
||||||
return HttpStatus.OK
|
try {
|
||||||
|
return await this.healthService.isDatabaseConnected();
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
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 {}
|
||||||
|
|||||||
14
api/src/health/health.service.spec.ts
Normal file
14
api/src/health/health.service.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HealthService } from './health.service';
|
||||||
|
|
||||||
|
describe('HealthService', () => {
|
||||||
|
let service: HealthService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [HealthService]
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<HealthService>(HealthService);
|
||||||
|
});
|
||||||
|
});
|
||||||
22
api/src/health/health.service.ts
Normal file
22
api/src/health/health.service.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CustomError } from '../error/customError';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HealthService {
|
||||||
|
constructor(private dataSource: DataSource) {}
|
||||||
|
|
||||||
|
async isDatabaseConnected(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
|
||||||
|
|
||||||
|
if (isDatabaseConnected) {
|
||||||
|
return isDatabaseConnected;
|
||||||
|
} else {
|
||||||
|
throw new CustomError('Database not connected', 'Database not connected', 400)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
api/src/interfaces/customJwtPayload.interface.ts
Normal file
5
api/src/interfaces/customJwtPayload.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { JwtPayload } from "jwt-decode";
|
||||||
|
|
||||||
|
export interface CustomJwtPayload extends JwtPayload {
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
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,7 +7,6 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -20,7 +19,6 @@ 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();
|
||||||
|
|
||||||
@@ -32,29 +30,6 @@ 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()
|
||||||
@@ -70,9 +45,22 @@ export class LogController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Public()
|
||||||
|
async findAll(): Promise<LogEntity[]> {
|
||||||
|
try {
|
||||||
|
return await this.logService.findAll();
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Body() logDto: LogDto) {
|
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
||||||
try {
|
try {
|
||||||
return await this.logService.create(logDto);
|
return await this.logService.create(logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -87,7 +75,7 @@ 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 {
|
||||||
return await this.logService.update(id, logDto);
|
return await this.logService.update(id, logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PilotEntity } from "src/pilot/pilot.entity";
|
||||||
|
|
||||||
export class LogDto {
|
export class LogDto {
|
||||||
pilotId: string;
|
pilotId: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { PilotEntity } from '../pilot/pilot.entity';
|
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||||
import { TrackEntity } from '../track/track.entity';
|
import { TrackEntity } from 'src/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' })
|
||||||
@@ -26,131 +25,52 @@ export class LogEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
routeTo: string;
|
routeTo: string;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column()
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer()
|
|
||||||
})
|
|
||||||
durationOfFlight: number;
|
durationOfFlight: number;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
singleEngineLand: number | null;
|
singleEngineLand: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
simulatorAtd: number | null;
|
simulatorAtd: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
landingsDay: number | null;
|
landingsDay: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
landingsNight: number | null;
|
landingsNight: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
groundTrainingReceived: number | null;
|
groundTrainingReceived: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
flightTrainingReceived: number | null;
|
flightTrainingReceived: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
crossCountry: number | null;
|
crossCountry: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
night: number | null;
|
night: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
solo: number | null;
|
solo: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
pilotInCommand: number | null;
|
pilotInCommand: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
instrumentActual: number | null;
|
instrumentActual: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
instrumentSimulated: number | null;
|
instrumentSimulated: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
instrumentApproaches: number | null;
|
instrumentApproaches: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
instrumentHolds: number | null;
|
instrumentHolds: number | null;
|
||||||
|
|
||||||
@Column('numeric', {
|
@Column({ nullable: true })
|
||||||
nullable: true,
|
|
||||||
precision: 10,
|
|
||||||
scale: 1,
|
|
||||||
transformer: new ColumnNumericTransformer(),
|
|
||||||
})
|
|
||||||
instrumentNavTrack: number | null;
|
instrumentNavTrack: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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';
|
||||||
|
|
||||||
@@ -15,9 +16,10 @@ export class LogInterceptor implements NestInterceptor {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return handler.handle().pipe(
|
return handler.handle().pipe(
|
||||||
map((data: any) => {
|
map((data: LogEntity[]) => {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const limitData = (log: LogEntity) => {
|
const limitData = (data) => {
|
||||||
|
return data.map((log: LogEntity) => {
|
||||||
return {
|
return {
|
||||||
id: log.id,
|
id: log.id,
|
||||||
pilot: {
|
pilot: {
|
||||||
@@ -30,47 +32,26 @@ export class LogInterceptor implements NestInterceptor {
|
|||||||
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 = jwtDecode(token);
|
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
|
||||||
|
|
||||||
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
if (jwtPayload.roles.includes('Flying.Read')) {
|
||||||
if (data.entities) {
|
const logs = limitData(data);
|
||||||
const logs = data.entities.map((entity) => limitData(data.entities));
|
|
||||||
|
|
||||||
return {
|
|
||||||
entities: logs,
|
|
||||||
total: data.total,
|
|
||||||
hasNextPage: data.hasNextPage
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const log = limitData(data);
|
|
||||||
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return logs;
|
||||||
} else {
|
} else {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} else if (!req.headers.authorization && isPublic) {
|
} else if (!req.headers.authorization && isPublic) {
|
||||||
if (data.entities) {
|
const publicData = limitData(data)
|
||||||
const publicData = data.entities.map((entity) => limitData(entity))
|
|
||||||
const logs = publicData.slice(0, 5)
|
const logs = publicData.slice(0, 5)
|
||||||
|
|
||||||
return {
|
return logs;
|
||||||
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 '../pilot/pilot.module';
|
import { PilotModule } from 'src/pilot/pilot.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
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,11 +3,10 @@ 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 '../pilot/pilot.service';
|
import { PilotService } from 'src/pilot/pilot.service';
|
||||||
import { PilotEntity } from '../pilot/pilot.entity';
|
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||||
import { CustomError } from '../error/customError';
|
import { CustomError } from 'src/error/customError';
|
||||||
import { FileService } from '../file/file.service';
|
import { FileService } from 'src/file/file.service';
|
||||||
import { Logs } from './logs.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogService {
|
export class LogService {
|
||||||
@@ -18,65 +17,42 @@ export class LogService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async find(id: string): Promise<LogEntity> {
|
async find(id: string): Promise<LogEntity> {
|
||||||
return await this.logRepository.findOne({
|
const logEntity: LogEntity = await this.logRepository.findOne({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
relations: ['pilot', 'tracks']
|
relations: ['pilot', 'tracks']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return logEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<LogEntity[]> {
|
||||||
|
return await this.logRepository.find({
|
||||||
|
relations: ['pilot', 'tracks']
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findLogsWithCount(skip?: number, take?: number): Promise<Logs> {
|
async create(logDto: LogDto): Promise<InsertResult> {
|
||||||
// const [entities, total] = await this.logRepository.findAndCount({
|
try{
|
||||||
// take,
|
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
|
||||||
// skip,
|
|
||||||
// order: {
|
|
||||||
// date: 'DESC'
|
|
||||||
// },
|
|
||||||
// relations: ['pilot', 'tracks']
|
|
||||||
// })
|
|
||||||
|
|
||||||
const [entities, total] = await this.logRepository
|
if (pilotEntity) {
|
||||||
.createQueryBuilder('logs')
|
const { pilotId, ...newLogDto } = logDto;
|
||||||
.innerJoinAndSelect('logs.pilot', 'pilot')
|
const log = this.logRepository.create({
|
||||||
.orderBy('logs.date', 'DESC')
|
...newLogDto,
|
||||||
.skip(skip)
|
pilot: pilotEntity
|
||||||
.take(take)
|
})
|
||||||
.getManyAndCount()
|
|
||||||
|
|
||||||
return {
|
return this.logRepository.insert(log);
|
||||||
entities,
|
} else {
|
||||||
total,
|
throw new CustomError('Pilot not found', 'Not found', 404);
|
||||||
hasNextPage: skip + take < total
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
|
async update(id: string, log: LogDto): Promise<UpdateResult> {
|
||||||
const [entities, total] = await this.logRepository
|
return await this.logRepository.update(id, log);
|
||||||
.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> {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { LogEntity } from "src/log/log.entity";
|
|
||||||
|
|
||||||
export interface Logs {
|
|
||||||
entities: LogEntity[],
|
|
||||||
total: number,
|
|
||||||
hasNextPage: boolean
|
|
||||||
}
|
|
||||||
@@ -9,12 +9,16 @@ 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: process.env.SESSION_SECRET,
|
secret: 'blah',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false
|
saveUninitialized: false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PilotEntity } from '../pilot/pilot.entity';
|
import { PilotEntity } from 'src/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' })
|
||||||
|
|||||||
@@ -1,260 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -50,8 +50,9 @@ export class PilotController {
|
|||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Body() pilotDto: PilotDto): Promise<PilotDto> {
|
async create(@Body() pilotDto: PilotDto) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
return await this.pilotService.create(pilotDto);
|
return await this.pilotService.create(pilotDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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 '../log/log.entity';
|
import { LogEntity } from 'src/log/log.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||||
import { CertificateEntity } from '../certificate/certificate.entity';
|
import { CertificateEntity } from '../certificate/certificate.entity';
|
||||||
import { EndorsementEntity } from '../endorsement/endorsement.entity';
|
import { EndorsementEntity } from 'src/endorsement/endorsement.entity';
|
||||||
import { MedicalEntity } from '../medical/medical.entity';
|
import { MedicalEntity } from 'src/medical/medical.entity';
|
||||||
|
|
||||||
@Entity({ name: 'pilots' })
|
@Entity({ name: 'pilots' })
|
||||||
export class PilotEntity {
|
export class PilotEntity {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
@@ -29,11 +30,9 @@ export class PilotInterceptor implements NestInterceptor {
|
|||||||
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);
|
if (jwtPayload.roles.includes('Flying.Read')) {
|
||||||
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;
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
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 '../error/customError';
|
import { CustomError } from 'src/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: PilotEntity = await this.pilotRepository.findOneBy({ id });
|
const pilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||||
|
|
||||||
if (pilotEntity) {
|
if (pilotEntity) {
|
||||||
return pilotEntity
|
return pilotEntity
|
||||||
@@ -29,18 +29,15 @@ export class PilotService {
|
|||||||
return await this.pilotRepository.find();
|
return await this.pilotRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(pilot: PilotDto): Promise<PilotDto> {
|
async create(pilot: PilotDto): Promise<InsertResult> {
|
||||||
return await this.pilotRepository.save(pilot);
|
return await this.pilotRepository.insert(pilot);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
id: string,
|
id: string,
|
||||||
pilotDto: PilotDto
|
pilot: PilotDto
|
||||||
): Promise<PilotDto> {
|
): Promise<UpdateResult> {
|
||||||
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id })
|
return await this.pilotRepository.update(id, pilot);
|
||||||
const pilotEntityUpdated = Object.assign(pilotEntity, pilotDto)
|
|
||||||
|
|
||||||
return await this.pilotRepository.save(pilotEntityUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<DeleteResult> {
|
async delete(id: string): Promise<DeleteResult> {
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
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 '../log/log.entity';
|
import { LogEntity } from 'src/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 '../log/log.module';
|
import { LogModule } from 'src/log/log.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -1,346 +0,0 @@
|
|||||||
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,11 +3,10 @@ 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 "../log/log.service";
|
import { LogService } from "src/log/log.service";
|
||||||
import { LogEntity } from "../log/log.entity";
|
import { LogEntity } from "src/log/log.entity";
|
||||||
import { CustomError } from "../error/customError";
|
import { CustomError } from "src/error/customError";
|
||||||
import { FileService } from "../file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
import { Logs } from "src/log/logs.interface";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrackService {
|
export class TrackService {
|
||||||
@@ -20,7 +19,11 @@ 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[]> {
|
||||||
@@ -37,11 +40,10 @@ export class TrackService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return tracks;
|
return tracks;
|
||||||
} else {
|
|
||||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
console.log(error)
|
||||||
|
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,11 +69,21 @@ 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> {
|
||||||
return await this.fileService.downloadFile(this.containerName, logId, fileName);
|
try {
|
||||||
|
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||||
|
|
||||||
|
return downloadedFile;
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export class ColumnNumericTransformer {
|
|
||||||
to(data: number): number {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
from (data: string): number {
|
|
||||||
return parseFloat(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
BLAH BLAH
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.3",
|
"version": "2.0.0-alpha-3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -14,15 +14,20 @@
|
|||||||
"@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",
|
||||||
|
"@heroui/react": "^3.0.0-beta.2",
|
||||||
|
"@heroui/styles": "^3.0.0-beta.2",
|
||||||
|
"@noahspan/noahspan-components": "^2.0.0-alpha-14",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@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",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"oidc-spa": "^7.2.4",
|
"oidc-spa": "^7.2.4",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-hook-form": "^7.51.4",
|
||||||
"react-leaflet": "^4",
|
"react-leaflet": "^4",
|
||||||
"react-leaflet-kml": "^2.1.2",
|
"react-leaflet-kml": "^2.1.2",
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Route, Routes } from 'react-router-dom';
|
import { Navigate, 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 { useNavigate } from 'react-router-dom';
|
import { useHref, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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>
|
||||||
</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: import.meta.env.VITE_ISSUER_URI,
|
issuerUri: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}/v2.0`,
|
||||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||||
homeUrl: import.meta.env.VITE_BASE_URL,
|
homeUrl: import.meta.env.VITE_BASE_URL,
|
||||||
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_CLIENT_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
|
||||||
}));
|
}));
|
||||||
93
client/src/components/actionMenu/ActionMenu.tsx
Normal file
93
client/src/components/actionMenu/ActionMenu.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { IActionMenuProps } from './IActionMenuProps';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Icon,
|
||||||
|
IconName,
|
||||||
|
Dropdown
|
||||||
|
} from '@noahspan/noahspan-components';
|
||||||
|
import { FormMode } from '../../enums/formMode';
|
||||||
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
|
||||||
|
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
||||||
|
// const [anchorElAction, setAnchorElAction] = useState<null | 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;
|
||||||
8
client/src/components/actionMenu/IActionMenuProps.tsx
Normal file
8
client/src/components/actionMenu/IActionMenuProps.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface AlertProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
closeIcon?: React.ReactNode;
|
|
||||||
onClose?: () => void;
|
|
||||||
severity: 'info' | 'error' | 'success' | 'warning';
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
// 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';
|
||||||
@@ -11,25 +21,31 @@ const ConfirmationDialog = ({
|
|||||||
title
|
title
|
||||||
}: DialogConfirmationProps) => {
|
}: DialogConfirmationProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal
|
||||||
<input type='checkbox' id='dialog' className='modal-toggle' onChange={() => {}} checked={isOpen} />
|
isDismissable={false}
|
||||||
<div className='modal' role='dialog'>
|
isKeyboardDismissDisabled={true}
|
||||||
<div className='modal-box'>
|
isOpen={isOpen}
|
||||||
<h3 className="text-lg font-bold">{title}</h3>
|
>
|
||||||
<p className="py-4">{contentText}</p>
|
<ModalContent>
|
||||||
<div className='modal-action'>
|
<ModalHeader>{title}</ModalHeader>
|
||||||
<button className='btn' onClick={onCancel}>
|
<ModalBody>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
{!isLoading && <div>{contentText}</div>}
|
||||||
|
{isLoading && <Spinner size='lg' />}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onCancel} startContent={<FontAwesomeIcon icon={faXmark} />}>
|
||||||
No
|
No
|
||||||
</button>
|
</Button>
|
||||||
<button className='btn btn-primary' onClick={onConfirm}>
|
<Button
|
||||||
<FontAwesomeIcon icon={faCircleCheck} />
|
color='primary'
|
||||||
|
onPress={onConfirm}
|
||||||
|
startContent={<FontAwesomeIcon icon={faCircleCheck} />}
|
||||||
|
>
|
||||||
Yes
|
Yes
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</ModalFooter>
|
||||||
</div>
|
</ModalContent>
|
||||||
</div>
|
</Modal>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,73 @@
|
|||||||
|
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 { useOidc } from "../../auth/oidcConfig";
|
import { Alert } from '@heroui/react'
|
||||||
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 { isUserLoggedIn } = useOidc();
|
const { logs, logsLoading } = useLogs();
|
||||||
const { screenSize } = useBreakpoints();
|
|
||||||
|
|
||||||
const getFlights = async (pageIndex: number, pageSize: number) => {
|
|
||||||
try {
|
|
||||||
const response: AxiosResponse = await httpClient.get(`api/logs/flights`, {
|
|
||||||
params: {
|
|
||||||
skip: pageIndex,
|
|
||||||
take: pageSize
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const flights: LogbookEntry[] = response.data.entities;
|
|
||||||
const hasMore: boolean = response.data.hasNextPage;
|
|
||||||
const total: number = response.data.total;
|
|
||||||
|
|
||||||
if (flights.length > 0) {
|
|
||||||
const newFlights: LogbookEntry[] = [...state.flights, ...flights]
|
|
||||||
const newPageIndex: number = state.pageIndex + flights.length;
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_FLIGHTS', payload: { flights: newFlights, hasMoreFlights: hasMore, pageIndex: newPageIndex, totalFlights: total }})
|
|
||||||
|
|
||||||
if (!isUserLoggedIn && flights.length >= 5) {
|
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'A limited number of flights displayed. Sign in to view all flights.'}})
|
|
||||||
} else {
|
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const axiosError = error as AxiosError;
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_ALERT',
|
|
||||||
payload: { severity: 'error', message: `Loading of flights failed with the following message: ${axiosError.message}`}
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMore = () => {
|
|
||||||
getFlights(state.pageIndex, state.pageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getFlights(state.pageIndex, state.pageSize);
|
|
||||||
}, [])
|
const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => {
|
||||||
|
if (log.tracks && log.tracks.length > 0) {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (flights && flights.length > 0) {
|
||||||
|
dispatch({ type: 'SET_FLIGHTS', payload: flights})
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No flights found' }})
|
||||||
|
}
|
||||||
|
}, [logs])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`max-w-screen-lg mx-auto ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD? 'mr-4 ml-4' : ''}`}>
|
<div className='max-w-screen-lg mx-auto'>
|
||||||
<div className='prose mt-5 mb-5'>
|
<div className='prose mt-5 mb-5'>
|
||||||
<h1>Flights</h1>
|
<h1>Flights</h1>
|
||||||
</div>
|
</div>
|
||||||
{!state.isLoading && state.alert && (
|
{!logsLoading && state.alert && (
|
||||||
<div>
|
<div>
|
||||||
<Alert
|
<Alert
|
||||||
className='mb-5'
|
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
color={state.alert.severity}
|
||||||
>
|
title={state.alert.message}
|
||||||
{state.alert.message}
|
/>
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!state.isLoading &&
|
{!logsLoading &&
|
||||||
<div>
|
|
||||||
<div>
|
<div>
|
||||||
<LogbookCard logs={state.flights} mode='flights' />
|
<LogbookCard logs={state.flights} mode='flights' />
|
||||||
</div>
|
</div>
|
||||||
{state.hasMoreFlights &&
|
|
||||||
<div className='flex items-center justify-center mb-5'>
|
|
||||||
<button className='btn btn-link' onClick={loadMore}>Load more</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
{logsLoading && [...Array(6)].map((_element, index) => {
|
||||||
}
|
|
||||||
{state.isLoading && [...Array(5)].map((_element, index) => {
|
|
||||||
return (
|
return (
|
||||||
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
<div className='mb-5'>
|
||||||
<div className='card-body' key={index}>
|
<Card
|
||||||
<div className='skeleton h-10 w-[150px]' />
|
key={index}
|
||||||
<div className='skeleton h-[350px]' />
|
>
|
||||||
<div className='skeleton h-[65px]' />
|
<div className='p-5'>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[60px]' width='w-[200px]' /></div>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[30px]' width='w-[200px]' /></div>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[300px]' width='w-[300px]' /></div>
|
||||||
|
{[...Array(4)].map((_element, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||||
|
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,8 +5,4 @@ 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,17 +5,13 @@ 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: { flights: LogbookEntry[], hasMoreFlights: boolean, pageIndex: number, totalFlights: number } }
|
| { type: 'SET_FLIGHTS'; payload: LogbookEntry[] }
|
||||||
| { 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 = (
|
||||||
@@ -32,10 +28,7 @@ export const reducer = (
|
|||||||
case 'SET_FLIGHTS': {
|
case 'SET_FLIGHTS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
flights: action.payload.flights,
|
flights: action.payload
|
||||||
hasMoreFlights: action.payload.hasMoreFlights,
|
|
||||||
pageIndex: action.payload.pageIndex,
|
|
||||||
totalFlights: action.payload.totalFlights
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'SET_IS_LOADING': {
|
case 'SET_IS_LOADING': {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useReducer } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
import { useForm, Controller, useFormContext } from 'react-hook-form';
|
import { Alert, Button, DatePicker, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Input, NumberInput, Select, Selection, SelectItem, SharedSelection, Textarea } from '@heroui/react'
|
||||||
|
import { useForm, Controller, FormProvider, useFormContext } from 'react-hook-form';
|
||||||
import { LogFormProps } from './LogFormProps.interface';
|
import { LogFormProps } from './LogFormProps.interface';
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
@@ -9,6 +10,7 @@ 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 = () => {
|
||||||
@@ -38,7 +40,7 @@ const LogForm = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'danger', message: axiosError.message }});
|
||||||
} finally {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||||
}
|
}
|
||||||
@@ -53,16 +55,11 @@ 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]);
|
||||||
@@ -76,21 +73,25 @@ const LogForm = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="pilotId"
|
name="pilotId"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { value } }) => {
|
||||||
return (
|
return (
|
||||||
<select
|
<Select
|
||||||
className='select w-full'
|
|
||||||
aria-labelledby='pilot'
|
aria-labelledby='pilot'
|
||||||
disabled={state.isDisabled}
|
isDisabled={state.isDisabled}
|
||||||
onChange={onChange}
|
fullWidth={true}
|
||||||
value={[value]}
|
isRequired={true}
|
||||||
|
onSelectionChange={(keys: SharedSelection) => {
|
||||||
|
setValue('pilotId', keys.currentKey);
|
||||||
|
}}
|
||||||
|
selectedKeys={[value]}
|
||||||
|
size='lg'
|
||||||
>
|
>
|
||||||
{state.pilotOptions?.map((pilotOption: { label: string, value: string; }) => {
|
{state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => {
|
||||||
return (
|
return (
|
||||||
<option value={pilotOption.value}>{pilotOption.label}</option>
|
<SelectItem key={pilotOption.key}>{pilotOption.label}</SelectItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</select>
|
</Select>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -103,13 +104,20 @@ 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(
|
||||||
<input
|
<DatePicker
|
||||||
type='date'
|
aria-labelledby='date'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
isRequired={true}
|
||||||
onChange={onChange}
|
onChange={(selectedDate) => {
|
||||||
value={value ? value.split('T')[0] : ''}
|
let date = selectedDate as CalendarDate;
|
||||||
|
|
||||||
|
setValue('date', date.toDate(getLocalTimeZone()).toISOString())
|
||||||
|
}}
|
||||||
|
size='lg'
|
||||||
|
value={parsedAbsoluteDate ? new CalendarDate(parsedAbsoluteDate.year, parsedAbsoluteDate.month, parsedAbsoluteDate.day) : value}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -123,11 +131,18 @@ const LogForm = () => {
|
|||||||
name="aircraftMakeModel"
|
name="aircraftMakeModel"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
aria-labelledby='aircraftMakeModel'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={formState.errors.address ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
formState.errors.address
|
||||||
|
? formState.errors.address.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isRequired={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
size='lg'
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -143,11 +158,18 @@ const LogForm = () => {
|
|||||||
name="aircraftIdentity"
|
name="aircraftIdentity"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
aria-labelledby='aircraftIdentity'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={formState.errors.address ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
formState.errors.address
|
||||||
|
? formState.errors.address.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isRequired={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
size='lg'
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -163,11 +185,18 @@ const LogForm = () => {
|
|||||||
name="routeFrom"
|
name="routeFrom"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
aria-labelledby='routeFrom'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={formState.errors.address ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
formState.errors.address
|
||||||
|
? formState.errors.address.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isRequired={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
size='lg'
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -181,11 +210,18 @@ const LogForm = () => {
|
|||||||
name="routeTo"
|
name="routeTo"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
aria-labelledby='routeTo'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={formState.errors.address ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
formState.errors.address
|
||||||
|
? formState.errors.address.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isRequired={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
size='lg'
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -199,11 +235,21 @@ const LogForm = () => {
|
|||||||
name="durationOfFlight"
|
name="durationOfFlight"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='durationOfFlight'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={formState.errors.address ? 'danger' : undefined}
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -219,11 +265,21 @@ const LogForm = () => {
|
|||||||
name="singleEngineLand"
|
name="singleEngineLand"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='singleEngineLand'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={formState.errors.address ? 'danger' : undefined}
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -237,11 +293,20 @@ const LogForm = () => {
|
|||||||
name="simulatorAtd"
|
name="simulatorAtd"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='simulaterAtd'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -258,11 +323,22 @@ const LogForm = () => {
|
|||||||
name="landingsDay"
|
name="landingsDay"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='landingsDay'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -276,11 +352,22 @@ const LogForm = () => {
|
|||||||
name="landingsNight"
|
name="landingsNight"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='landingsNight'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -299,12 +386,24 @@ const LogForm = () => {
|
|||||||
name="groundTrainingReceived"
|
name="groundTrainingReceived"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='groundTrainingReceived'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -319,11 +418,22 @@ const LogForm = () => {
|
|||||||
name="flightTrainingReceived"
|
name="flightTrainingReceived"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='flightTrainingReceived'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -337,11 +447,22 @@ const LogForm = () => {
|
|||||||
name="crossCountry"
|
name="crossCountry"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='crossCountry'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -355,11 +476,22 @@ const LogForm = () => {
|
|||||||
name="night"
|
name="night"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='night'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -373,11 +505,22 @@ const LogForm = () => {
|
|||||||
name="solo"
|
name="solo"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='solo'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -391,11 +534,22 @@ const LogForm = () => {
|
|||||||
name="pilotInCommand"
|
name="pilotInCommand"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='pilotInCommand'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -412,11 +566,22 @@ const LogForm = () => {
|
|||||||
name="instrumentActual"
|
name="instrumentActual"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='instrumentActual'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -430,11 +595,22 @@ const LogForm = () => {
|
|||||||
name="instrumentSimulated"
|
name="instrumentSimulated"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='instrumentSimulated'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -450,11 +626,22 @@ const LogForm = () => {
|
|||||||
name="instrumentApproaches"
|
name="instrumentApproaches"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='instrumentApproaches'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -468,11 +655,22 @@ const LogForm = () => {
|
|||||||
name="instrumentHolds"
|
name="instrumentHolds"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='instrumentHolds'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -486,13 +684,23 @@ const LogForm = () => {
|
|||||||
name="instrumentNavTrack"
|
name="instrumentNavTrack"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<NumberInput
|
||||||
type='number'
|
aria-labelledby='instrumentNavTrack'
|
||||||
className='input w-full'
|
isDisabled={state.isDisabled}
|
||||||
disabled={state.isDisabled}
|
color={
|
||||||
min={0}
|
formState.errors.address ? 'danger' : undefined
|
||||||
onChange={(event) => onChange(Number(event.target.value))}
|
}
|
||||||
step={0.1}
|
errorMessage={
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -508,12 +716,19 @@ const LogForm = () => {
|
|||||||
name="notes"
|
name="notes"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<textarea
|
<Textarea
|
||||||
className='textarea w-full'
|
aria-labelledby='notes'
|
||||||
disabled={state.isDisabled}
|
isDisabled={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,9 +1,13 @@
|
|||||||
|
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;
|
||||||
pilotOptions: { label: string; value: string; }[];
|
landingsSelectedKeys: Selection;
|
||||||
|
pilotOptions: { key: string; label: string; }[];
|
||||||
selectedPilotName: string;
|
selectedPilotName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
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_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
| { type: 'SET_LANDINGS_SELECTED_KEYS'; payload: Selection }
|
||||||
|
| { 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: ''
|
||||||
};
|
};
|
||||||
@@ -27,18 +34,36 @@ 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,7 +1,10 @@
|
|||||||
import { useEffect, useReducer, useState } from 'react';
|
import { Key, useEffect, useReducer } 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';
|
||||||
@@ -11,42 +14,21 @@ 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, faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight } from '@fortawesome/free-solid-svg-icons'
|
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, HeaderContext, PaginationState, useReactTable } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, 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 [columnVisibility, setColumnVisibility] = useState({});
|
const logbookContext = useLogbookContext()
|
||||||
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;
|
||||||
|
|
||||||
@@ -72,7 +54,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).replace('Z', ''));
|
const date = new Date(info.getValue() as string);
|
||||||
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
@@ -87,17 +69,13 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'route',
|
id: 'route',
|
||||||
header: 'Route of Flight',
|
header: 'Route of Flight',
|
||||||
meta: {
|
meta: {
|
||||||
className: 'border-l border-base-300',
|
headerAlign: 'center'
|
||||||
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',
|
||||||
@@ -111,9 +89,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
accessorKey: 'durationOfFlight',
|
accessorKey: 'durationOfFlight',
|
||||||
header: 'Duration Of Flight',
|
header: 'Duration Of Flight',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'text-right',
|
align: 'right',
|
||||||
className: 'border-l border-base-300',
|
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) : '',
|
||||||
@@ -122,27 +99,65 @@ 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: 'text-center',
|
align: 'center',
|
||||||
className: 'border-l border-base-300',
|
headerAlign: 'center'
|
||||||
headerAlign: 'text-center'
|
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
return (
|
return (
|
||||||
<Actions id={info.row.original.id} />
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly variant='light' size='lg'>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownSection showDivider>
|
||||||
|
<DropdownItem
|
||||||
|
key='edit'
|
||||||
|
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faPen} />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key='view'
|
||||||
|
onPress={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faEye} />}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
<DropdownSection>
|
||||||
|
<DropdownItem
|
||||||
|
key='Delete'
|
||||||
|
onPress={() => onDeleteLog(info.row.original.id)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<LogbookEntry>[] = [
|
const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
||||||
|
pilotName,
|
||||||
|
date,
|
||||||
|
aircraftMakeModel,
|
||||||
|
route,
|
||||||
|
durationOfFlight,
|
||||||
|
notes
|
||||||
|
]
|
||||||
|
|
||||||
|
const authColumns: ColumnDef<LogbookEntry>[] = [
|
||||||
pilotName,
|
pilotName,
|
||||||
date,
|
date,
|
||||||
aircraftMakeModel,
|
aircraftMakeModel,
|
||||||
@@ -158,8 +173,8 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
accessorKey: 'singleEngineLand',
|
accessorKey: 'singleEngineLand',
|
||||||
header: 'Single Engine Land',
|
header: 'Single Engine Land',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : '',
|
||||||
@@ -169,8 +184,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'landings',
|
id: 'landings',
|
||||||
header: 'Landings',
|
header: 'Landings',
|
||||||
meta: {
|
meta: {
|
||||||
className: 'border-l border-base-300',
|
headerAlign: 'center'
|
||||||
headerAlign: 'text-center'
|
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -179,9 +193,8 @@ 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: 'text-right',
|
align: 'right',
|
||||||
className: 'border-l border-base-300',
|
headerAlign: 'right'
|
||||||
headerAlign: 'text-right'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -190,8 +203,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: 'right'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -200,8 +213,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
id: 'instrument',
|
id: 'instrument',
|
||||||
header: 'Instrument',
|
header: 'Instrument',
|
||||||
meta: {
|
meta: {
|
||||||
className: 'border-l border-base-300',
|
headerAlign: 'center'
|
||||||
headerAlign: 'text-center'
|
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -210,9 +222,8 @@ 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: 'text-right',
|
align: 'right',
|
||||||
className: 'border-l border-base-300',
|
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) : '',
|
||||||
@@ -223,8 +234,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : ''
|
||||||
@@ -235,8 +246,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: 'right'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -245,8 +256,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: 'right'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -255,8 +266,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: 'right'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -265,8 +276,7 @@ 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: {
|
||||||
className: 'border-l border-base-300',
|
headerAlign: 'center'
|
||||||
headerAlign: 'text-center'
|
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -275,9 +285,8 @@ 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: 'text-right',
|
align: 'right',
|
||||||
className: 'border-l border-base-300',
|
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) : ''
|
||||||
@@ -288,8 +297,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : ''
|
||||||
@@ -300,8 +309,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : ''
|
||||||
@@ -312,8 +321,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : ''
|
||||||
@@ -324,8 +333,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : ''
|
||||||
@@ -336,8 +345,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: 'text-right',
|
align: 'right',
|
||||||
headerAlign: 'text-right'
|
headerAlign: '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) : ''
|
||||||
@@ -346,87 +355,31 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
},
|
},
|
||||||
notes,
|
notes,
|
||||||
actions
|
actions
|
||||||
];
|
]
|
||||||
|
|
||||||
const onPaginationChange = (updater: any) => {
|
const getLogbookEntries = async () => {
|
||||||
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`);
|
||||||
params: {
|
const entries: LogbookEntry[] = response.data;
|
||||||
skip: pageIndex * pageSize,
|
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
take: pageSize
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.entities.length > 0) {
|
if (response.data.length > 0) {
|
||||||
const entries: LogbookEntry[] = response.data.entities;
|
dispatch({ type: 'SET_ENTRIES', payload: response.data });
|
||||||
const entryColumns: string[] = Object.keys(entries[0]);
|
|
||||||
const columnVisibility: {[key: string]: boolean} = {}
|
|
||||||
|
|
||||||
for (const column of table.getAllLeafColumns()) {
|
if (state.alert) {
|
||||||
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: 'info', message: 'No logbook entries found.'}})
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', 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: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', 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 });
|
||||||
@@ -478,13 +431,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(state.pagination?.pageIndex, state.pagination?.pageSize);
|
await getLogbookEntries();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', 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 });
|
||||||
@@ -498,100 +451,96 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRowsPerPageChanged = (event: any) => {
|
// useEffect(() => {
|
||||||
const newPaginationState: PaginationState = {
|
// let newColumns: ColumnDef<LogbookEntry>[];
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
// if (userRole === UserRole.WRITE) {
|
||||||
}
|
// newColumns = [...authColumns];
|
||||||
|
// } else {
|
||||||
|
// newColumns = [...unauthColumns];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const actionsColumnExists = newColumns.find((column) => column.id === 'actions');
|
||||||
|
// const tracksColumnExists = newColumns.find((column) => column.id === 'actions');
|
||||||
|
|
||||||
|
// if (!actionsColumnExists) {
|
||||||
|
// newColumns.push(actionsColumn);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!tracksColumnExists) {
|
||||||
|
// const notesColumnIndex = newColumns.findIndex((column) => column.id === 'notes')
|
||||||
|
|
||||||
|
// newColumns.splice(notesColumnIndex, 0, tracksColumn)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// dispatch({ type: 'SET_COLUMNS', payload: newColumns })
|
||||||
|
// }, [isUserLoggedIn])
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: state.entries,
|
||||||
|
columns: authColumns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel()
|
||||||
|
});
|
||||||
|
|
||||||
|
const textAlignment = {
|
||||||
|
center: 'text-center',
|
||||||
|
left: 'text-start',
|
||||||
|
right: 'text-end'
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logbookContext.state.isDrawerOpen) {
|
if (!logbookContext.state.isDrawerOpen) {
|
||||||
getLogbookEntries(state.pagination.pageIndex, state.pagination.pageSize);
|
getLogbookEntries();
|
||||||
}
|
}
|
||||||
}, [logbookContext.state.isDrawerOpen, state.pagination.pageIndex, state.pagination.pageSize]);
|
}, [logbookContext.state.isDrawerOpen]);
|
||||||
|
|
||||||
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={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||||
<div className='prose max-w-none col-span-6 mt-5 mb-5'>
|
<div className='prose max-w-none col-span-10 mt-5 mb-5'>
|
||||||
<h1>Logbook</h1>
|
<h1>Logbook</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-6 justify-self-end self-center'>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
{userRole === UserRole.WRITE &&
|
{userRole === UserRole.WRITE &&
|
||||||
<button className='btn btn-primary'
|
<Button
|
||||||
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
|
color='primary'
|
||||||
|
onPress={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faAdd} />}
|
||||||
|
data-testid="pilot-add-button"
|
||||||
>
|
>
|
||||||
<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 })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
color={'default'}
|
||||||
>
|
title={state.alert.message}
|
||||||
{state.alert.message}
|
/>
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD && (
|
{!state.isLoading && (
|
||||||
<div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg '>
|
<div className='col-span-12'>
|
||||||
<div className='overflow-x-auto mb-5'>
|
{state.entries.length > 0 && screenSize !== ScreenSize.SM && (
|
||||||
<div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'>
|
<div className='p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto shadow-small rounded-large w-full'>
|
||||||
<label className='select select-sm select-ghost'>
|
<table className='min-w-full h-auto table-auto w-full'>
|
||||||
<span className='label'>Rows per page</span>
|
<thead className='[&>tr]:first:rounded-lg'>
|
||||||
<select
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
defaultValue={1}
|
|
||||||
onChange={onRowsPerPageChanged}
|
|
||||||
value={state.pagination?.pageSize}
|
|
||||||
>
|
|
||||||
<option value={10}>10</option>
|
|
||||||
<option value={25}>25</option>
|
|
||||||
<option value={50}>50</option>
|
|
||||||
<option value={state.totalEntries}>All</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<table className='table min-w-full h-auto table-auto w-full'>
|
|
||||||
<thead className='bg-base-200'>
|
|
||||||
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
|
||||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header, headerIndex) => {
|
{headerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={`${header.column.columnDef.meta?.className ? header.column.columnDef.meta?.className : ''} group/th px-3 h-10 align-middle whitespace-nowrap text-foreground-500 text-tiny font-semibold ${headerGroupIndex === 0 ? 'first:rounded-tl-lg last:rounded-tr-lg' : ''} ${headerGroupIndex === table.getHeaderGroups().length - 1 ? 'first:rounded-bl-lg last:rounded-br-lg' : ''} data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
className={`${header.column.columnDef.meta?.align ? textAlignment[header.column.columnDef.meta?.align] : ''} group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
key={header.id}
|
key={header.id}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder ? null : (
|
{header.isPlaceholder ? null : (
|
||||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
<div>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
@@ -617,15 +566,13 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
return (
|
return (
|
||||||
<td
|
<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`}
|
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}
|
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()
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -634,7 +581,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
</tbody>
|
</tbody>
|
||||||
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
<thead className='[&>tr]:first:rounded-lg'>
|
||||||
{table.getFooterGroups().map((footerGroup, index) => {
|
{table.getFooterGroups().map((footerGroup, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -642,18 +589,16 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
{footerGroup.headers.map((header) => {
|
{footerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
className='roup/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
|
className='group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
|
||||||
key={header.id}
|
key={header.id}
|
||||||
|
align={header.column.columnDef.meta?.align}
|
||||||
>
|
>
|
||||||
<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()
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -664,103 +609,22 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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 &&
|
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<div className='col-span-12'>
|
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} />
|
||||||
<>
|
|
||||||
{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 className='col-span-12 justify-self-center'>
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* {state.isLoading && !state.alert && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Loading size='xl' />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
{logbookContext.state.isDrawerOpen && (
|
{logbookContext.state.isDrawerOpen && (
|
||||||
<LogbookDrawer
|
<LogbookDrawer
|
||||||
@@ -777,6 +641,14 @@ 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, PaginationState } from '@tanstack/react-table';
|
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||||
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,7 +10,4 @@ export interface LogbookState {
|
|||||||
isConfirmDialogLoading: boolean;
|
isConfirmDialogLoading: boolean;
|
||||||
isConfirmDialogOpen: boolean;
|
isConfirmDialogOpen: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
pages: number[];
|
|
||||||
pagination: PaginationState;
|
|
||||||
totalEntries: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
290
client/src/components/logbook/columns.tsx
Normal file
290
client/src/components/logbook/columns.tsx
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
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, PaginationState } from '@tanstack/react-table';
|
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||||
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,12 +7,11 @@ 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: { entries: LogbookEntry[], totalEntries: number } }
|
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
|
||||||
| { 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,
|
||||||
@@ -20,13 +19,7 @@ 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 = (
|
||||||
@@ -49,8 +42,7 @@ export const reducer = (
|
|||||||
case 'SET_ENTRIES': {
|
case 'SET_ENTRIES': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
entries: action.payload.entries,
|
entries: action.payload
|
||||||
totalEntries: action.payload.totalEntries
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_ALERT': {
|
case 'SET_ALERT': {
|
||||||
@@ -71,18 +63,6 @@ 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,23 +1,21 @@
|
|||||||
|
import { Accordion, AccordionItem, Card, CardContent, 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) => {
|
||||||
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 className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
|
||||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-2' : ''}`} key={log.id}>
|
|
||||||
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
|
||||||
<div>
|
<div>
|
||||||
|
<Card className='p-4' key={log.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className='font-bold text-2xl'>{formattedDate}</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
{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
|
||||||
@@ -27,12 +25,9 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
<Accordion variant='surface'>
|
||||||
<div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
|
<AccordionItem key='1'>
|
||||||
<input type='checkbox' />
|
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
|
||||||
<div className='collapse-title font-semibold'>Details</div>
|
|
||||||
<div className='collapse-content'>
|
|
||||||
<div className={`grid grid-cols-12 gap-3 ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'mr-[5%] ml-[5%]' : 'mr-[30%] ml-[30%]'} mt-4 mb-4`}>
|
|
||||||
<div className='col-span-6 font-bold'>
|
<div className='col-span-6 font-bold'>
|
||||||
<span>Aircraft Make and Model</span>
|
<span>Aircraft Make and Model</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,9 +63,11 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionItem>
|
||||||
</div>
|
</Accordion>
|
||||||
</div>
|
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { Alert, Button, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Tab, Tabs } from "@heroui/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";
|
||||||
@@ -10,11 +9,10 @@ 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 { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
import { Key, useState } from "react";
|
||||||
import { ScreenSize } from "../../enums/screenSize";
|
|
||||||
|
|
||||||
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('time');
|
const [activeTab, setActiveTab] = useState<Key>('time');
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
pilotId: '',
|
pilotId: '',
|
||||||
date: null,
|
date: null,
|
||||||
@@ -42,7 +40,6 @@ 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);
|
||||||
@@ -52,7 +49,6 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: unknown) => {
|
const onSubmit = async (data: unknown) => {
|
||||||
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) {
|
||||||
@@ -62,116 +58,115 @@ 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: 'error', message: axiosError.message }});
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', 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 onTabClicked = (event: any) => {
|
const onSelectedKeyChanged = (key: React.Key) => {
|
||||||
setActiveTab(event.target.ariaLabel)
|
setActiveTab(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='drawer drawer-end'>
|
<Drawer
|
||||||
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={logbookContext.state.isDrawerOpen} />
|
closeButton={
|
||||||
<div className="drawer-side">
|
<Button isIconOnly>
|
||||||
<label
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
htmlFor='my-drawer-1'
|
</Button>
|
||||||
aria-label='close-sidebar'
|
}
|
||||||
className='drawer-overlay'
|
isOpen={logbookContext.state.isDrawerOpen}
|
||||||
></label>
|
onClose={onCancel}
|
||||||
<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">
|
|
||||||
<h2 className="mt-0 mb-0 self-center">
|
|
||||||
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||||
</h2>
|
</DrawerHeader>
|
||||||
</div>
|
<DrawerBody>
|
||||||
<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 && (
|
{logbookContext.state.formAlert && (
|
||||||
|
<div className='col-span-12'>
|
||||||
<Alert
|
<Alert
|
||||||
className='mb-5'
|
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={logbookContext.state.formAlert.severity}
|
color={logbookContext.state.formAlert.severity}
|
||||||
>
|
title={logbookContext.state.formAlert.message}
|
||||||
{logbookContext.state.formAlert.message}
|
/>
|
||||||
</Alert>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='tabs tabs-lift mb-5'>
|
<Tabs
|
||||||
<label className='tab'>
|
onSelectionChange={onSelectedKeyChanged}
|
||||||
<input aria-label='time' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'time' ? true : false} />
|
selectedKey={activeTab as string}
|
||||||
<FontAwesomeIcon className='mr-1'icon={faClock} />
|
>
|
||||||
|
<Tabs.ListContainer>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab
|
||||||
|
id='time'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClock} />
|
||||||
Time
|
Time
|
||||||
</label>
|
<Tabs.Indicator />
|
||||||
<div className='tab-content border-base-300 p-6'>
|
</Tabs.Tab>
|
||||||
<LogForm />
|
|
||||||
</div>
|
|
||||||
{logbookContext.state.formMode !== FormMode.ADD &&
|
{logbookContext.state.formMode !== FormMode.ADD &&
|
||||||
<>
|
<Tabs.Tab
|
||||||
<label className='tab'>
|
id='tracks'
|
||||||
<input aria-label='tracks' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'tracks' ? true : false} />
|
>
|
||||||
<FontAwesomeIcon className='mr-1' icon={faMapLocationDot} />
|
<FontAwesomeIcon icon={faMapLocationDot} />
|
||||||
Tracks
|
Tracks
|
||||||
</label>
|
<Tabs.Indicator />
|
||||||
<div className='tab-content border-base-300 p-6'>
|
</Tabs.Tab>
|
||||||
<TracksForm />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
</div>
|
</Tabs.List>
|
||||||
|
</Tabs.ListContainer>
|
||||||
|
<Tabs.Panel id='time'>
|
||||||
|
<LogForm />
|
||||||
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel id='tracks'>
|
||||||
|
<TracksForm />
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</DrawerBody>
|
||||||
{activeTab !== 'tracks' &&
|
{activeTab !== 'tracks' &&
|
||||||
<div>
|
<DrawerFooter>
|
||||||
<div className='grid grid-cols-12 gap-3'>
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
<div className='col-span-12 justify-self-end self-center'>
|
<div className='col-span-12 justify-self-end self-center'>
|
||||||
<button
|
<Button
|
||||||
className='btn'
|
isDisabled={
|
||||||
disabled={
|
|
||||||
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
|
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
|
||||||
? logbookContext.state.isFormDisabled
|
? logbookContext.state.isFormDisabled
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
onClick={onCancel}
|
onPress={onCancel}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||||
</button>
|
</Button>
|
||||||
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
|
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
|
||||||
<button
|
<Button
|
||||||
className='btn btn-primary ml-2.5'
|
className='ml-[10px]'
|
||||||
disabled={logbookContext.state.isFormDisabled}
|
isDisabled={logbookContext.state.isFormDisabled}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant='primary'
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faSave} />
|
<FontAwesomeIcon icon={faSave} />
|
||||||
Save
|
Save
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DrawerFooter>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</DrawerContent>
|
||||||
</div>
|
</Drawer>
|
||||||
</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,19 +1,18 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { Key, 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,
|
||||||
@@ -45,7 +44,39 @@ 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);
|
||||||
@@ -107,46 +138,43 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}, [pilotId]);
|
}, [pilotId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='drawer drawer-end'>
|
<Drawer
|
||||||
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={isDrawerOpen} />
|
closeButton={
|
||||||
<div className='drawer-side'>
|
<Button isIconOnly>
|
||||||
<label
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
htmlFor='my-drawer-1'
|
</Button>
|
||||||
aria-label='close-sidebar'
|
}
|
||||||
className='drawer-overlay'
|
isOpen={isDrawerOpen}
|
||||||
></label>
|
placement='right'
|
||||||
<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%]'}`}>
|
data-testid="pilot-drawer"
|
||||||
|
onClose={onCancel}
|
||||||
|
size='xl'
|
||||||
|
>
|
||||||
|
<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 gap-3'>
|
<DrawerHeader>
|
||||||
<div className="col-span-10">
|
|
||||||
<h2 className="mt-0 mb-0 self-center">
|
|
||||||
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||||
</h2>
|
</DrawerHeader>
|
||||||
</div>
|
<DrawerBody>
|
||||||
<div className="col-span-2 justify-self-end self-center">
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
<button className="btn btn-ghost" onClick={onCancel}>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-3 self-center'>
|
<div className='col-span-3 self-center'>
|
||||||
<span>Name *</span>
|
<h6>Name *</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-9'>
|
<div className='col-span-9'>
|
||||||
<Controller
|
<Autocomplete
|
||||||
name="name"
|
inputValue={peoplePickerValue}
|
||||||
control={methods.control}
|
isLoading={isPeoplePickerLoading}
|
||||||
rules={{ required: 'A name is required' }}
|
items={peoplePickerResults}
|
||||||
render={({ field: { onChange, value } }) => (
|
onInputChange={onPeoplePickerSearch}
|
||||||
<input
|
onSelectionChange={(key: Key | null) => onPersonSelected(key as string)}
|
||||||
type='text'
|
>
|
||||||
className='input w-full'
|
{peoplePickerResults.map((person: Person) => (
|
||||||
disabled={isDisabled}
|
<AutocompleteItem key={person.userPrincipalName}>
|
||||||
onChange={onChange}
|
{person.displayName}
|
||||||
value={value}
|
</AutocompleteItem>
|
||||||
/>
|
))}
|
||||||
)}
|
</Autocomplete>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{isUserLoggedIn &&
|
{isUserLoggedIn &&
|
||||||
<>
|
<>
|
||||||
@@ -159,10 +187,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'An address is required' }}
|
rules={{ required: 'An address is required' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
|
||||||
className='input w-full'
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.address ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.address
|
||||||
|
? methods.formState.errors.address.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
@@ -178,10 +211,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'A city is required' }}
|
rules={{ required: 'A city is required' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
|
||||||
className='input w-full'
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.city ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.city
|
||||||
|
? methods.formState.errors.city.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
@@ -197,15 +235,14 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'A state must be selected' }}
|
rules={{ required: 'A state must be selected' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<select
|
<Select
|
||||||
className='select w-full'
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={[value]}
|
selectedKeys={[value]}
|
||||||
>
|
>
|
||||||
{states.map((state) => (
|
{states.map((state) => (
|
||||||
<option key={state.value}>{state.label}</option>
|
<SelectItem key={state.value}>{state.label}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,10 +255,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
control={methods.control}
|
control={methods.control}
|
||||||
rules={{ required: 'A postal code is required' }}
|
rules={{ required: 'A postal code is required' }}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
|
||||||
className='input w-full'
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.postalCode ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.postalCode
|
||||||
|
? methods.formState.errors.postalCode.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
@@ -242,10 +284,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
|
||||||
className='input w-full'
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.email ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.email
|
||||||
|
? methods.formState.errors.email.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
@@ -266,10 +313,15 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<input
|
<Input
|
||||||
type='text'
|
|
||||||
className='input w-full'
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.phone ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.phone
|
||||||
|
? methods.formState.errors.phone.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
@@ -293,39 +345,38 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
</Grid> */}
|
</Grid> */}
|
||||||
{/* <div className='col-span-12 justify-self-end self-center'> */}
|
{/* <div className='col-span-12 justify-self-end self-center'> */}
|
||||||
|
|
||||||
|
{/* </div> */}
|
||||||
<div className='col-span-12 justify-self-end self-center'>
|
</div>
|
||||||
<button
|
</DrawerBody>
|
||||||
className='btn'
|
<DrawerFooter>
|
||||||
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
isDisabled && mode.toString() !== FormMode.VIEW
|
isDisabled && mode.toString() !== FormMode.VIEW
|
||||||
? isDisabled
|
? isDisabled
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
onClick={onCancel}
|
startContent={<FontAwesomeIcon icon={faXmark} />}
|
||||||
|
onPress={onCancel}
|
||||||
data-testid="pilot-cancel-button"
|
data-testid="pilot-cancel-button"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||||
</button>
|
</Button>
|
||||||
{mode.toString() !== FormMode.VIEW && (
|
{mode.toString() !== FormMode.VIEW && (
|
||||||
<button
|
<Button
|
||||||
className='btn btn-primary ml-2.5'
|
color='primary'
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
startContent={<FontAwesomeIcon icon={faSave} />}
|
||||||
type="submit"
|
type="submit"
|
||||||
data-testid="pilot-save-button"
|
data-testid="pilot-save-button"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faSave} />
|
|
||||||
Save
|
Save
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DrawerFooter>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</DrawerContent>
|
||||||
</div>
|
</Drawer>
|
||||||
</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,44 +4,20 @@ 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, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } 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 {
|
||||||
@@ -58,7 +34,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: 'info', message: 'No pilots found.' }})
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }})
|
||||||
dispatch({ type: 'SET_PILOTS', payload: [] });
|
dispatch({ type: 'SET_PILOTS', payload: [] });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,7 +42,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of pilots failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', 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 })
|
||||||
@@ -125,7 +101,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', 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 });
|
||||||
@@ -139,39 +115,66 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<Pilot>[]= [
|
const columns = [
|
||||||
{
|
{
|
||||||
id: 'name',
|
id: 'name',
|
||||||
accessorKey: 'name',
|
name: 'Name'
|
||||||
header: 'Name'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: 'Actions',
|
name: 'Actions'
|
||||||
meta: {
|
|
||||||
align: 'text-center',
|
|
||||||
headerAlign: 'text-center'
|
|
||||||
},
|
|
||||||
cell: (info: CellContext<Pilot, unknown>) => {
|
|
||||||
return (
|
|
||||||
<Actions id={info.row.original.id} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const textAlignment = {
|
const renderCell = (pilot: any, columnKey: any) => {
|
||||||
center: 'text-center',
|
const cellValue = pilot[columnKey]
|
||||||
left: 'text-start',
|
|
||||||
right: 'text-end'
|
|
||||||
};
|
|
||||||
|
|
||||||
const table = useReactTable({
|
switch (columnKey) {
|
||||||
data: state.pilots,
|
case 'actions': {
|
||||||
columns: columns,
|
return (
|
||||||
getCoreRowModel: getCoreRowModel(),
|
<Dropdown>
|
||||||
getPaginationRowModel: getPaginationRowModel()
|
<Dropdown.Trigger>
|
||||||
});
|
<Button isIconOnly size='lg'>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
|
</Button>
|
||||||
|
</Dropdown.Trigger>
|
||||||
|
<Dropdown.Popover>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Section>
|
||||||
|
<Dropdown.Item
|
||||||
|
key='edit'
|
||||||
|
onPress={() => onOpenClosePilotForm(FormMode.EDIT, pilot.id)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPen} />
|
||||||
|
Edit
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
key='view'
|
||||||
|
onPress={() => onOpenClosePilotForm(FormMode.VIEW, pilot.id)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
View
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Section>
|
||||||
|
<Dropdown.Section>
|
||||||
|
<Dropdown.Item
|
||||||
|
key='Delete'
|
||||||
|
onPress={() => onDeletePilot(pilot.id)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
Delete
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Section>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown.Popover>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return cellValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isFormOpen) {
|
if (!state.isFormOpen) {
|
||||||
@@ -181,128 +184,64 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||||
<div className='prose max-w-none col-span-6 mt-5 mb-5' >
|
<div className='prose max-w-none col-span-10 mt-5 mb-5' >
|
||||||
<h1>Pilots</h1>
|
<h1>Pilots</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-6 justify-self-end self-center'>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
{!state.isLoading && userRole === UserRole.WRITE &&
|
{userRole === UserRole.WRITE &&
|
||||||
<button
|
<Button
|
||||||
className='btn btn-primary'
|
color='primary'
|
||||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
onPress={() => 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 })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
color={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
|
|
||||||
className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''} group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold rounded data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
|
||||||
colSpan={header.colSpan}
|
|
||||||
key={header.id}
|
|
||||||
>
|
>
|
||||||
{header.isPlaceholder ? null : (
|
{column.name}
|
||||||
<div>
|
</TableColumn>
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
)}
|
||||||
{/* {header.column.getCanFilter() ? (
|
</TableHeader>
|
||||||
<div>
|
<TableBody items={state.pilots}>
|
||||||
<Filter column={header.column} table={table} />
|
{(item) => (
|
||||||
</div>
|
<TableRow key={item.id}>
|
||||||
) : null} */}
|
{(columnKey => (
|
||||||
</div>
|
<TableCell>
|
||||||
)}
|
{renderCell(item, columnKey)}
|
||||||
</th>
|
</TableCell>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</thead>
|
</TableRow>
|
||||||
<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>
|
</TableBody>
|
||||||
);
|
</Table>
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<div className='col-span-12'>
|
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
|
||||||
<>
|
|
||||||
{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>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{state.isFormOpen && (
|
{state.isFormOpen && (
|
||||||
<PilotForm
|
<PilotForm
|
||||||
isDrawerOpen={state.isFormOpen}
|
isDrawerOpen={state.isFormOpen}
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ 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 { faBars, faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
import { faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { 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',
|
||||||
@@ -54,39 +53,13 @@ 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',
|
||||||
@@ -106,62 +79,53 @@ const SiteNav = () => {
|
|||||||
}, [isUserLoggedIn]);
|
}, [isUserLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-base-100 shadow-sm w-full">
|
<Navbar isBordered maxWidth='full' position='static'>
|
||||||
<div className={`navbar-start ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
|
<NavbarContent>
|
||||||
{screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
|
<NavbarBrand>
|
||||||
<div className="dropdown">
|
<img
|
||||||
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
|
height={35}
|
||||||
<FontAwesomeIcon icon={faBars} size='xl' />
|
width={35}
|
||||||
</div>
|
src='noahspan-logo.png'
|
||||||
<ul
|
style={{ marginRight: '5px' }}
|
||||||
tabIndex={-1}
|
/>
|
||||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
<FontAwesomeIcon icon={faPlane} size='2x' />
|
||||||
<Links />
|
</NavbarBrand>
|
||||||
</ul>
|
</NavbarContent>
|
||||||
</div>
|
<NavbarContent justify='center'>
|
||||||
) : (
|
{pages.length > 0 && pages.map((page, index) => {
|
||||||
<Brand />
|
return (
|
||||||
)}
|
<NavbarItem isActive={pathname === page.path ? true : false} key={index}>
|
||||||
</div>
|
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}>
|
||||||
<div className="navbar-center">
|
{page.name}
|
||||||
<ul className="menu menu-horizontal px-1">
|
</Link>
|
||||||
{screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
|
</NavbarItem>
|
||||||
<Brand />
|
)
|
||||||
) : (
|
})}
|
||||||
<Links />
|
</NavbarContent>
|
||||||
)}
|
<NavbarContent justify='end'>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className={`navbar-end ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'mr-8' : ''}`}>
|
|
||||||
{!isUserLoggedIn &&
|
{!isUserLoggedIn &&
|
||||||
<button className='btn btn-ghost' onClick={() => login()}><FontAwesomeIcon icon={faSignIn} />Sign In</button>
|
<Button
|
||||||
|
color='default'
|
||||||
|
onPress={() => login()}
|
||||||
|
startContent={<FontAwesomeIcon icon={faSignIn} />}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
{isUserLoggedIn &&
|
{isUserLoggedIn &&
|
||||||
<div className='dropdown dropdown-end'>
|
<Dropdown>
|
||||||
<div tabIndex={0} role='button'>
|
<DropdownTrigger>
|
||||||
<div className={`avatar ${userPhoto ? userPhoto : 'avatar-placeholder'}`}>
|
<Avatar name={appContext.state.userProfile.displayName?.toString()} src={userPhoto}></Avatar>
|
||||||
{userPhoto &&
|
</DropdownTrigger>
|
||||||
<div className='w-12 rounded-full'>
|
<DropdownMenu>
|
||||||
<img src={userPhoto} />
|
<DropdownItem key='signout' onPress={() => logout({redirectTo: 'specific url', url: '/'})} startContent={<FontAwesomeIcon icon={faSignOut} />}>
|
||||||
</div>
|
Sign Out
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
}
|
}
|
||||||
{!userPhoto &&
|
</NavbarContent>
|
||||||
<div className='bg-neutral text-neutral-content w-10 rounded-full'>
|
</Navbar>
|
||||||
<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>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
53
client/src/components/trackMap/TrackMap.css
Normal file
53
client/src/components/trackMap/TrackMap.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#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,6 +4,10 @@ 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'
|
||||||
|
|
||||||
@@ -37,7 +41,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={7}
|
zoom={8}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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";
|
||||||
@@ -27,7 +28,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: 'error', message: axiosError.message }})
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +108,11 @@ const TracksForm = () => {
|
|||||||
|
|
||||||
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);
|
||||||
@@ -114,25 +120,26 @@ const TracksForm = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='col-span-10'>
|
<div className='col-span-10'>
|
||||||
<input className='input w-full' disabled={state.isDisabled} key={index} readOnly type='text' value={filename} />
|
<Input disabled={state.isDisabled} key={index} type='text' value={filename}/>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2'>
|
<div className='col-span-2'>
|
||||||
<button className='btn w-full' disabled={state.isDisabled} key={index} onClick={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></button>
|
<Button isDisabled={state.isDisabled} key={index} isIconOnly onPress={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{logbookContext.state.formMode === FormMode.EDIT &&
|
|
||||||
<div className='col-span-12'>
|
<div className='col-span-12'>
|
||||||
<label
|
<Button
|
||||||
className='btn cursor-pointer w-full'
|
as='label'
|
||||||
|
color='primary'
|
||||||
|
isDisabled={state.isDisabled}
|
||||||
|
fullWidth={true}
|
||||||
|
startContent={<FontAwesomeIcon icon={faUpload} />}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUpload} />
|
|
||||||
Upload Track
|
Upload Track
|
||||||
<input className='hidden' id='track-upload' onChange={handleFileUpload} type='file' />
|
<input hidden onChange={handleFileUpload} type='file' />
|
||||||
</label>
|
</Button>
|
||||||
</div>
|
</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
|
||||||
}
|
}
|
||||||
9
client/src/globals.css
Normal file
9
client/src/globals.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "@heroui/styles";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
3
client/src/hero.ts
Normal file
3
client/src/hero.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// hero.ts
|
||||||
|
import { heroui } from "@heroui/react";
|
||||||
|
export default heroui();
|
||||||
@@ -14,17 +14,17 @@ export const useBreakpoints = () => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case width >= 640 && width < 1024: {
|
case width >= 640: {
|
||||||
size = ScreenSize.MD;
|
size = ScreenSize.MD;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case width >= 1024 && width < 1280: {
|
case width >= 1024: {
|
||||||
size = ScreenSize.LG
|
size = ScreenSize.LG
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case width >= 1280 && width < 1536: {
|
case width >= 1280: {
|
||||||
size = ScreenSize.XL;
|
size = ScreenSize.XL;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ export const useUserRole = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isUserLoggedIn && decodedIdToken) {
|
if (isUserLoggedIn && decodedIdToken) {
|
||||||
const rolesKeyName: string | undefined = Object.keys(decodedIdToken).find((key) => key.includes('roles'));
|
const idTokenRoles: string[] = decodedIdToken!.roles as string[];
|
||||||
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 style="background-color: #f5f5f5;">
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface Alert {
|
export interface Alert {
|
||||||
severity: 'info' | 'error' | 'success' | 'warning';
|
severity: 'danger' | 'default' | 'success' | 'warning';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -5,7 +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';
|
import './globals.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<OidcProvider>
|
<OidcProvider>
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@plugin "daisyui";
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "lofi";
|
|
||||||
default: true;
|
|
||||||
prefersdark: false;
|
|
||||||
color-scheme: "light";
|
|
||||||
--color-base-100: oklch(100% 0 0);
|
|
||||||
--color-base-200: oklch(97% 0 0);
|
|
||||||
--color-base-300: oklch(94% 0 0);
|
|
||||||
--color-base-content: oklch(0% 0 0);
|
|
||||||
--color-primary: oklch(15.906% 0 0);
|
|
||||||
--color-primary-content: oklch(100% 0 0);
|
|
||||||
--color-secondary: oklch(21.455% 0.001 17.278);
|
|
||||||
--color-secondary-content: oklch(100% 0 0);
|
|
||||||
--color-accent: oklch(26.861% 0 0);
|
|
||||||
--color-accent-content: oklch(100% 0 0);
|
|
||||||
--color-neutral: oklch(0% 0 0);
|
|
||||||
--color-neutral-content: oklch(100% 0 0);
|
|
||||||
--color-info: oklch(79.54% 0.103 205.9);
|
|
||||||
--color-info-content: oklch(15.908% 0.02 205.9);
|
|
||||||
--color-success: oklch(90.13% 0.153 164.14);
|
|
||||||
--color-success-content: oklch(18.026% 0.03 164.14);
|
|
||||||
--color-warning: oklch(88.37% 0.135 79.94);
|
|
||||||
--color-warning-content: oklch(17.674% 0.027 79.94);
|
|
||||||
--color-error: oklch(78.66% 0.15 28.47);
|
|
||||||
--color-error-content: oklch(15.732% 0.03 28.47);
|
|
||||||
--radius-selector: 0.5rem;
|
|
||||||
--radius-field: 0.5rem;
|
|
||||||
--radius-box: 0.5rem;
|
|
||||||
--size-selector: 0.25rem;
|
|
||||||
--size-field: 0.25rem;
|
|
||||||
--border: 1px;
|
|
||||||
--depth: 0;
|
|
||||||
--noise: 0;
|
|
||||||
}
|
|
||||||
@plugin "@tailwindcss/typography";
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
5
client/src/tanstack.d.ts
vendored
5
client/src/tanstack.d.ts
vendored
@@ -3,9 +3,8 @@ 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?: 'text-left' | 'text-center' | 'text-right';
|
align?: 'left' | 'center' | 'right';
|
||||||
className?: string;
|
headerAlign?: 'left' | 'center' | 'right';
|
||||||
headerAlign?: 'text-left' | 'text-center' | 'text-right';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|||||||
2
client/src/vite-env.d.ts
vendored
2
client/src/vite-env.d.ts
vendored
@@ -3,9 +3,7 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_BASE_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_USERINFO_ENDPOINT: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
dbs:
|
dbs:
|
||||||
- path: /mnt/data/flying.db
|
- path: /var/lib/data/flying.db
|
||||||
replicas:
|
replicas:
|
||||||
- path: /mnt/data/backup/flying.db
|
- path: /mnt/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:/mnt/data/flying.db
|
- ./database/flying.db:/var/lib/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:/mnt/data
|
- data:/var/lib/data
|
||||||
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /mnt/data/flying.db
|
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /var/lib/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=../../mnt/data/flying.db
|
- DB_PATH=../../var/lib/data/flying.db
|
||||||
volumes:
|
volumes:
|
||||||
- data:/mnt/data
|
- data:/var/lib/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:/mnt/data
|
- data:/var/lib/data
|
||||||
command: replicate -config /mnt/litestream/litestream.yml
|
command: replicate -config /mnt/litestream/litestream.yml
|
||||||
depends_on:
|
depends_on:
|
||||||
app:
|
app:
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# data "azuread_application" "app_registration" {
|
||||||
|
# provider = azuread.external_tenant
|
||||||
|
# display_name = module.environment.app_reg_name
|
||||||
|
# }
|
||||||
|
|
||||||
resource "azurerm_container_app" "container_app" {
|
resource "azurerm_container_app" "container_app" {
|
||||||
name = module.environment.app_name
|
name = module.environment.app_name
|
||||||
container_app_environment_id = data.azurerm_container_app_environment.container_app_environment.id
|
container_app_environment_id = data.azurerm_container_app_environment.container_app_environment.id
|
||||||
@@ -9,7 +14,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", "/mnt/data/flying.db"]
|
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/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 +22,12 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "data"
|
name = "data"
|
||||||
path = "/mnt/data"
|
path = "/var/lib/data"
|
||||||
}
|
}
|
||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "backup"
|
name = "backup"
|
||||||
path = "/mnt/data/backup"
|
path = "/mnt/data"
|
||||||
sub_path = "data"
|
sub_path = "data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,12 +47,12 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "data"
|
name = "data"
|
||||||
path = "/mnt/data"
|
path = "/var/lib/data"
|
||||||
}
|
}
|
||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "backup"
|
name = "backup"
|
||||||
path = "/mnt/data/backup"
|
path = "/mnt/data"
|
||||||
sub_path = "data"
|
sub_path = "data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +65,7 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
container {
|
container {
|
||||||
cpu = 0.25
|
cpu = 0.25
|
||||||
image = "noahspan/flying:20589367895"
|
image = "noahspan/flying:19615036250"
|
||||||
memory = "0.5Gi"
|
memory = "0.5Gi"
|
||||||
name = "flying"
|
name = "flying"
|
||||||
|
|
||||||
@@ -69,16 +74,6 @@ 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"
|
||||||
value = var.CLIENT_ID
|
value = var.CLIENT_ID
|
||||||
@@ -89,26 +84,6 @@ 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.EXTERNAL_TENANT_ID
|
value = var.EXTERNAL_TENANT_ID
|
||||||
@@ -116,18 +91,18 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
env {
|
env {
|
||||||
name = "DB_PATH"
|
name = "DB_PATH"
|
||||||
value = "/mnt/data/flying.db"
|
value = "/var/lib/data/flying.db"
|
||||||
}
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
name = "DB_SYNC"
|
name = "DB_SYNC"
|
||||||
value = "true"
|
value = "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
startup_probe {
|
startup_probe {
|
||||||
failure_count_threshold = 3
|
failure_count_threshold = 10
|
||||||
initial_delay = 15
|
initial_delay = 1
|
||||||
interval_seconds = 30
|
interval_seconds = 2
|
||||||
path = "/api/health"
|
path = "/api/health"
|
||||||
port = 3000
|
port = 3000
|
||||||
transport = "HTTP"
|
transport = "HTTP"
|
||||||
@@ -135,7 +110,7 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
volume_mounts {
|
volume_mounts {
|
||||||
name = "data"
|
name = "data"
|
||||||
path = "/mnt/data"
|
path = "/var/lib/data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,11 +159,6 @@ 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 = "ReadWrite"
|
access_mode = "ReadOnly"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
variable "AUTHORITY" {
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "CLIENT_ID" {
|
variable "CLIENT_ID" {
|
||||||
type = string
|
type = string
|
||||||
@@ -20,14 +17,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -36,11 +25,6 @@ variable "EXTERNAL_TENANT_ID" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "SESSION_SECRET" {
|
|
||||||
sensitive = true
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "TENANT_ID" {
|
variable "TENANT_ID" {
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|||||||
2728
package-lock.json
generated
2728
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@noahspan/flying",
|
"name": "@noahspan/flying",
|
||||||
"version": "2.1.3",
|
"version": "2.0.0-alpha-3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node api/dist/main",
|
"start": "node api/dist/main",
|
||||||
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
"daisyui": "^5.5.5",
|
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user