Compare commits
106 Commits
104-switch
...
146-switch
| Author | SHA1 | Date | |
|---|---|---|---|
| e50213a1aa | |||
| a54105e8e5 | |||
| b8ec5be269 | |||
| 875b8a0d90 | |||
| 81c589001b | |||
| baedfc1675 | |||
| 4eccf8e83c | |||
| a4778d29be | |||
| 5c04870d37 | |||
| c81b514ae8 | |||
| 5a07318527 | |||
| 99e5d162ef | |||
| 755c429d04 | |||
| 6ea7326a57 | |||
| e3f385d0a4 | |||
| f33a9982cc | |||
| 100fd78551 | |||
| a87d35a5e4 | |||
| 0c4fe29553 | |||
| 03c4dacd99 | |||
| 1ed1a6c48a | |||
| 4c77ebc1d6 | |||
| 650ea704aa | |||
| d51e95f59e | |||
| 0806e114e1 | |||
| fc716c664b | |||
| 5a1e24a1ce | |||
| b93327f840 | |||
| bbcf6cb3de | |||
| 344d4f5625 | |||
| 22fbf7615b | |||
| 5a84c221d6 | |||
| 13f1e86e72 | |||
| 25f935171e | |||
| 92fec88ad1 | |||
| 833b1b107c | |||
| 674ae1cf3c | |||
| f0b61703b7 | |||
| c152f06b63 | |||
| 3c9d26e085 | |||
| 12b4f1a65b | |||
| ae03c0200c | |||
| b9668a9d48 | |||
| 19041e3967 | |||
| 5729c72fe1 | |||
| 8760998c78 | |||
| 61f8dc0e1e | |||
| cc7b694fd5 | |||
| 9553c2ec34 | |||
| 0a8c461e3d | |||
| 931d56e753 | |||
| 7ebf78f348 | |||
| 95e2d59abc | |||
| 945081dd7c | |||
| 7213dcf9e1 | |||
| a13ffeaebd | |||
| 620b258e94 | |||
| cc21a43825 | |||
| 112b56080c | |||
| 92f97a93bf | |||
| 4f6e9a072c | |||
| 0c8e4b9fba | |||
| 63d2765d19 | |||
| da76000b8d | |||
| b789e1264a | |||
| 9cc86fe79f | |||
| ee0e10072c | |||
| 899f92f342 | |||
| 5c4245179f | |||
| a7a9f01fd9 | |||
| a367b517db | |||
| fd514e4c8b | |||
| d8513e6dd0 | |||
| 5de5a165da | |||
| 26c46fe6e8 | |||
| 04e5e3e16b | |||
| 0305b424eb | |||
| 8652544471 | |||
| b7b6bdcf4a | |||
| 9c96774f7b | |||
| a9abb5863c | |||
| 477469831f | |||
| 6964531e12 | |||
| 08baaa6d83 | |||
| 487afb08d6 | |||
| b7e74242b8 | |||
| 876f1f65a2 | |||
| 4db1a0fb32 | |||
| f246c1f0f6 | |||
| 36b4018858 | |||
| 6b0c292d34 | |||
| 51de91f8d4 | |||
| 41e9ae33dc | |||
| 870241b37f | |||
| a827b5a39f | |||
| 0663ac873e | |||
| 7cfe005154 | |||
| f421b8d231 | |||
| 5977659dd4 | |||
| 01c1e7015e | |||
| 10d160bbc2 | |||
| f321c2506a | |||
| 976574c6ac | |||
| f3c7fb8853 | |||
| cf8e50fb9c | |||
| fec99878de |
@@ -10,17 +10,27 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
api
|
||||
client
|
||||
|
||||
- name: Get Secrets from Infisical
|
||||
uses: Infisical/secrets-action@v1.0.16
|
||||
with:
|
||||
domain: ${{ secrets.INFISICAL_DOMAIN }}
|
||||
client-id: ${{ secrets.INFISICAL_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
|
||||
project-slug: ${{ secrets.INFISICAL_PROJECT_SLUG }}
|
||||
env-slug: ${{ inputs.environment_name }}
|
||||
secret-path: /flying
|
||||
|
||||
- name: Install Nest CLI
|
||||
run: |
|
||||
npm install -g @nestjs/cli
|
||||
@@ -28,7 +38,7 @@ jobs:
|
||||
- name: 'Setup Node'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ vars.NODE_VERSION }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -38,7 +48,17 @@ jobs:
|
||||
run: |
|
||||
npm run build -w api
|
||||
|
||||
- name: Test API
|
||||
run: |
|
||||
npm run test -w api
|
||||
|
||||
- name: Build Client
|
||||
env:
|
||||
VITE_API_URL: ${{ env.VITE_API_URL }}
|
||||
VITE_BASE_URL: ${{ env.VITE_BASE_URL }}
|
||||
VITE_CLIENT_ID: ${{ env.VITE_CLIENT_ID }}
|
||||
VITE_ISSUER_URI: ${{ env.VITE_ISSUER_URI }}
|
||||
VITE_TENANT_ID: ${{ env.VITE_TENANT_ID }}
|
||||
run: |
|
||||
npm run build -w client
|
||||
|
||||
@@ -46,8 +66,8 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
@@ -7,6 +7,9 @@ on:
|
||||
client:
|
||||
value: ${{ jobs.changes.outputs.client }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: filter
|
||||
@@ -15,7 +18,13 @@ jobs:
|
||||
api: ${{ steps.filter.outputs.api }}
|
||||
client: ${{ steps.filter.outputs.client }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Setup Node'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
@@ -24,4 +33,6 @@ jobs:
|
||||
api:
|
||||
- 'api/**'
|
||||
client:
|
||||
- 'client/**'
|
||||
- 'client/**'
|
||||
infrastructure:
|
||||
- 'infrastructure/**'
|
||||
44
.gitea/workflows/deploy.yaml
Normal file
44
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Deploy
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
app_name:
|
||||
required: true
|
||||
type: string
|
||||
environment_name:
|
||||
required: true
|
||||
type: string
|
||||
version_number:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment_name }}
|
||||
steps:
|
||||
- name: Get Secrets from Infisical
|
||||
uses: Infisical/secrets-action@v1.0.16
|
||||
with:
|
||||
domain: ${{ secrets.INFISICAL_DOMAIN }}
|
||||
client-id: ${{ secrets.INFISICAL_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
|
||||
project-slug: ${{ secrets.INFISICAL_PROJECT_SLUG }}
|
||||
env-slug: ${{ inputs.environment_name }}
|
||||
secret-path: /flying
|
||||
|
||||
- name: Install Azure CLI
|
||||
run: |
|
||||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||
|
||||
- name: Log in to Azure
|
||||
run: |
|
||||
az login --service-principal --username ${{ env.AZURE_CLIENT_ID }} --password ${{ env.AZURE_CLIENT_SECRET }} --tenant ${{ env.AZURE_TENANT_ID }}
|
||||
|
||||
- name: Update Container App
|
||||
run: |
|
||||
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --container-name ${{ inputs.app_name }} --resource-group ${{ env.RESOURCE_GROUP }} --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
|
||||
@@ -6,14 +6,14 @@ on:
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
uses: ./.github/workflows/changes.yaml
|
||||
uses: ./.gitea/workflows/changes.yaml
|
||||
|
||||
build:
|
||||
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true' }}
|
||||
name: build
|
||||
build-and-test:
|
||||
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true' }}
|
||||
name: build-and-test
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/build.yaml
|
||||
uses: ./.gitea/workflows/build_and_test.yaml
|
||||
with:
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
@@ -23,8 +23,8 @@ jobs:
|
||||
name: deploy
|
||||
needs:
|
||||
- changes
|
||||
- build
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
- build-and-test
|
||||
uses: ./.gitea/workflows/deploy.yaml
|
||||
with:
|
||||
app_name: flying
|
||||
environment_name: test
|
||||
19
.gitea/workflows/pull_request.yaml
Normal file
19
.gitea/workflows/pull_request.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
uses: ./.gitea/workflows/changes.yaml
|
||||
|
||||
build:
|
||||
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true'}}
|
||||
name: build-and-test
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.gitea/workflows/build_and_test.yaml
|
||||
with:
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
@@ -2,12 +2,12 @@ name: Tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
uses: ./.github/workflows/build.yaml
|
||||
build-and-test:
|
||||
name: build-and-test
|
||||
uses: ./.gitea/workflows/build_and_test.yaml
|
||||
with:
|
||||
environment_name: prod
|
||||
version_number: ${{ github.ref_name }}
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
deploy:
|
||||
name: deploy
|
||||
needs:
|
||||
- build
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
- build-and-test
|
||||
uses: ./.gitea/workflows/deploy.yaml
|
||||
with:
|
||||
app_name: flying
|
||||
environment_name: prod
|
||||
44
.gitea/workflows/terraform.yaml
Normal file
44
.gitea/workflows/terraform.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
name: 'Terraform'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment_name:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
infrastructure
|
||||
|
||||
- name: Get Secrets from Infisical
|
||||
uses: Infisical/secrets-action@v1.0.16
|
||||
with:
|
||||
domain: ${{ secrets.INFISICAL_DOMAIN }}
|
||||
client-id: ${{ secrets.INFISICAL_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
|
||||
project-slug: ${{ secrets.INFISICAL_PROJECT_SLUG }}
|
||||
env-slug: ${{ inputs.environment_name }}
|
||||
secret-path: /flying/terraform
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v4
|
||||
|
||||
- name: Terraform init
|
||||
run: |
|
||||
terraform -chdir=./infrastructure init -reconfigure -backend-config="./backend/${{ inputs.environment_name }}.backend.tfvars" -input=false
|
||||
|
||||
- name: Terraform plan
|
||||
run: |
|
||||
terraform -chdir=./infrastructure plan -no-color -input=false
|
||||
|
||||
# - name: Terraform apply
|
||||
# if: ${{ github.event_name != 'pull_request' }}
|
||||
# run: |
|
||||
# terraform -chdir=./infrastructure apply -no-color -input=false
|
||||
|
||||
35
.github/workflows/deploy.yaml
vendored
35
.github/workflows/deploy.yaml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Deploy
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
app_name:
|
||||
required: true
|
||||
type: string
|
||||
environment_name:
|
||||
required: true
|
||||
type: string
|
||||
version_number:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment_name }}
|
||||
steps:
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
|
||||
- name: Azure CLI script
|
||||
uses: azure/cli@v2
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |
|
||||
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --container-name ${{ inputs.app_name }} --resource-group noahspan --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
|
||||
19
.github/workflows/pull_request.yaml
vendored
19
.github/workflows/pull_request.yaml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
uses: ./.github/workflows/changes.yaml
|
||||
|
||||
build:
|
||||
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true'}}
|
||||
name: build
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/build.yaml
|
||||
with:
|
||||
environment_name: pull_request
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/*.env
|
||||
.DS_Store
|
||||
.dapr
|
||||
**/**/secrets.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:22-slim
|
||||
FROM node:22
|
||||
|
||||
WORKDIR app
|
||||
COPY ./api/dist ./api/dist
|
||||
@@ -9,10 +9,8 @@ WORKDIR api
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /
|
||||
COPY ./api/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["node", "./app/api/dist/main.js"]
|
||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
24
README.md
24
README.md
@@ -1,3 +1,23 @@
|
||||
# Flying (React and NestJS)
|
||||
# Flying
|
||||
|
||||
A pilot logbook app
|
||||
A web app for tracking pilot flight hours and sharing flight tracks.
|
||||
|
||||
## Built With
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
### Installation
|
||||
|
||||
## Usage
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Expense tracking for plane rental, instructor fees, and fuel rates
|
||||
- [ ] Export logbook to CSV
|
||||
- [ ]
|
||||
|
||||
## License
|
||||
|
||||
## Contact
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Flying API
|
||||
|
||||
An API for the Flying app.
|
||||
An API for the Flying app.
|
||||
...
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
npx typeorm migration:run -d ./app/api/dist/database/data-source.js
|
||||
node ./app/api/dist/main.js
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.1.5",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -11,7 +11,8 @@ export const dataSourceOptions: DataSourceOptions = {
|
||||
database: configService.get<string>('DB_PATH'),
|
||||
entities: ['../**/*.entity.js'],
|
||||
migrations: ['./migrations/*.js'],
|
||||
synchronize: configService.get<boolean>('DB_SYNC')
|
||||
synchronize: configService.get<boolean>('DB_SYNC'),
|
||||
migrationsRun: true
|
||||
}
|
||||
|
||||
const dataSource = new DataSource(dataSourceOptions);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'endorsements' })
|
||||
|
||||
@@ -48,10 +48,10 @@ import { ConfigService } from '@nestjs/config';
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
async downloadFile(containerName: string, rowKey: string, fileName: string): Promise<string> {
|
||||
async downloadFile(containerName: string, logId: string, fileName: string): Promise<string> {
|
||||
this.containerName = containerName;
|
||||
|
||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
|
||||
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
||||
const downloadBlockBlobResponse = await blockBlobClient.download();
|
||||
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
|
||||
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { HealthController } from "./health.controller"
|
||||
import { HttpStatus } from "@nestjs/common";
|
||||
|
||||
describe('HealthController', () => {
|
||||
let controller; HealthController;
|
||||
|
||||
const mockHealthService = {
|
||||
isDatabaseConnected: jest.fn()
|
||||
}
|
||||
let controller: HealthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [HealthController],
|
||||
providers: [HealthService]
|
||||
controllers: [HealthController]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<HealthController>(HealthController);
|
||||
})
|
||||
|
||||
it('isHealthy => should return true', () => {
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return database connected', async () => {
|
||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(true);
|
||||
|
||||
const result = await controller.isHealthy();
|
||||
|
||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||
expect(result).toEqual(true);
|
||||
})
|
||||
|
||||
it('isHealthy => should return error', async () => {
|
||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
|
||||
|
||||
it('isHealth => should return status ok', async () => {
|
||||
const result = await controller.isHealthy();
|
||||
|
||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||
expect(result).toEqual(false);
|
||||
expect(result).toEqual(HttpStatus.OK);
|
||||
})
|
||||
})
|
||||
@@ -1,27 +1,13 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { HealthService } from './health.service';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly healthService: HealthService
|
||||
) {}
|
||||
|
||||
|
||||
@Get()
|
||||
async isHealthy(): Promise<boolean> {
|
||||
try {
|
||||
return await this.healthService.isDatabaseConnected();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
async isHealthy(): Promise<HttpStatus> {
|
||||
return HttpStatus.OK
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
HealthService
|
||||
]
|
||||
})
|
||||
export class HealthModule {}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
describe('HealthService', () => {
|
||||
let service: HealthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [HealthService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<HealthService>(HealthService);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async isDatabaseConnected(): Promise<boolean> {
|
||||
try {
|
||||
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
|
||||
|
||||
if (isDatabaseConnected) {
|
||||
return isDatabaseConnected;
|
||||
} else {
|
||||
throw new CustomError('Database not connected', 'Database not connected', 400)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { JwtPayload } from "jwt-decode";
|
||||
|
||||
export interface CustomJwtPayload extends JwtPayload {
|
||||
permissions: string[];
|
||||
}
|
||||
449
api/src/log/log.controller.spec.ts
Normal file
449
api/src/log/log.controller.spec.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LogController } from './log.controller';
|
||||
import { LogService } from './log.service';
|
||||
import { LogDto } from './log.dto';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('LogController', () => {
|
||||
let controller: LogController;
|
||||
|
||||
const mockLogService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findLogsWithCount: jest.fn(),
|
||||
findLogsWithTracks: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [LogController],
|
||||
providers: [
|
||||
ConfigService,
|
||||
FileService,
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mockLogService
|
||||
}
|
||||
]
|
||||
}).compile()
|
||||
|
||||
controller = module.get<LogController>(LogController);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
})
|
||||
|
||||
it('find => should find a log by id', async () => {
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(log);
|
||||
|
||||
const result = await controller.find(log.id);
|
||||
|
||||
expect(result).toEqual(log);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(log.id);
|
||||
})
|
||||
|
||||
it('find => should fail to find a log by id', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockRejectedValue(new Error('Log not found'))
|
||||
|
||||
try {
|
||||
await controller.find(id);
|
||||
|
||||
fail('find did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(id);
|
||||
expect(mockLogService.find).rejects.toThrow('Log not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should find logs with count', async () => {
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log]
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockLogService, 'findLogsWithCount').mockReturnValue({
|
||||
entities: logs,
|
||||
total: count,
|
||||
hasNextPage: morePages
|
||||
});
|
||||
|
||||
const {entities, total, hasNextPage} = await controller.findLogsWithCount();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(total).toEqual(count);
|
||||
expect(hasNextPage).toEqual(morePages)
|
||||
expect(mockLogService.findLogsWithCount).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should fail to find logs with count', async () => {
|
||||
jest.spyOn(mockLogService, 'findLogsWithCount').mockRejectedValue(new Error('Logs not found'))
|
||||
|
||||
try {
|
||||
await controller.findLogsWithCount();
|
||||
|
||||
fail('findLogsWithCount did not throw error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.findLogsWithCount).toHaveBeenCalled();
|
||||
expect(mockLogService.findLogsWithCount).rejects.toThrow('Logs not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('findLogsWithTracks => should find logs with tracks', async () => {
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log]
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockLogService, 'findLogsWithTracks').mockReturnValue({
|
||||
entities: logs,
|
||||
total: count,
|
||||
hasNextPage: morePages
|
||||
});
|
||||
|
||||
const {entities, total, hasNextPage} = await controller.findLogsWithTracks();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(total).toEqual(count);
|
||||
expect(hasNextPage).toEqual(morePages)
|
||||
expect(mockLogService.findLogsWithTracks).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should fail to find logs with tracks', async () => {
|
||||
jest.spyOn(mockLogService, 'findLogsWithTracks').mockRejectedValue(new Error('Logs not found'))
|
||||
|
||||
try {
|
||||
await controller.findLogsWithTracks();
|
||||
|
||||
fail('findLogsWithTracks did not throw error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.findLogsWithTracks).toHaveBeenCalled();
|
||||
expect(mockLogService.findLogsWithTracks).rejects.toThrow('Logs not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('create => should create a new log', async () => {
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto;
|
||||
|
||||
const log = {
|
||||
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockLogService, 'create').mockReturnValue(log);
|
||||
|
||||
const result = await controller.create(logDto);
|
||||
|
||||
expect(mockLogService.create).toHaveBeenCalled();
|
||||
expect(mockLogService.create).toHaveBeenCalledWith(logDto);
|
||||
expect(result).toEqual(log);
|
||||
})
|
||||
|
||||
it('create => should fail to create a new log', async () => {
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto;
|
||||
|
||||
jest.spyOn(mockLogService, 'create').mockRejectedValue(new Error('Log failed to create'))
|
||||
|
||||
try {
|
||||
await controller.create(logDto);
|
||||
|
||||
fail('create did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.create).toHaveBeenCalled();
|
||||
expect(mockLogService.create).toHaveBeenCalledWith(logDto);
|
||||
expect(mockLogService.create).rejects.toThrow('Log failed to create')
|
||||
}
|
||||
})
|
||||
|
||||
it('update => should update an existing log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto
|
||||
|
||||
jest.spyOn(mockLogService, 'update').mockReturnValue(logDto);
|
||||
|
||||
const result = await controller.update(id, logDto);
|
||||
|
||||
expect(result).toEqual(logDto);
|
||||
expect(mockLogService.update).toHaveBeenCalled();
|
||||
expect(mockLogService.update).toHaveBeenCalledWith(id, logDto);
|
||||
})
|
||||
|
||||
it('update => should fail to update an exising log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto;
|
||||
|
||||
jest.spyOn(mockLogService, 'update').mockRejectedValue(new Error('Log failed to update'))
|
||||
|
||||
try {
|
||||
await controller.update(id, logDto);
|
||||
|
||||
fail('update did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.update).toHaveBeenCalled();
|
||||
expect(mockLogService.update).toHaveBeenCalledWith(id, logDto);
|
||||
expect(mockLogService.update).rejects.toThrow('Log failed to update')
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete and existing log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
|
||||
jest.spyOn(mockLogService, 'delete');
|
||||
|
||||
const result = await controller.delete(id);
|
||||
|
||||
expect(mockLogService.delete).toHaveBeenCalled();
|
||||
expect(mockLogService.delete).toHaveBeenCalledWith(id);
|
||||
})
|
||||
|
||||
it('delete => should fail to delete and exising log', async () => {
|
||||
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
|
||||
jest.spyOn(mockLogService, 'delete').mockRejectedValue(new Error('Log failed to delete'));
|
||||
|
||||
try {
|
||||
await controller.delete(id);
|
||||
|
||||
fail('delete did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockLogService.delete).toHaveBeenCalled();
|
||||
expect(mockLogService.delete).toHaveBeenCalledWith(id);
|
||||
expect(mockLogService.delete).rejects.toThrow('Log failed to delete')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
@@ -19,6 +20,7 @@ import { LogInterceptor } from './log.interceptor';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Logs } from './logs.interface';
|
||||
|
||||
const reflector = new Reflector();
|
||||
|
||||
@@ -30,7 +32,30 @@ export class LogController {
|
||||
private readonly logService: LogService
|
||||
) {}
|
||||
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findLogsWithCount(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
|
||||
try {
|
||||
return await this.logService.findLogsWithCount(skip, take)
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@Get('flights')
|
||||
@Public()
|
||||
async findLogsWithTracks(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
|
||||
try {
|
||||
return await this.logService.findLogsWithTracks(skip, take)
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Public()
|
||||
async find(
|
||||
@@ -44,28 +69,15 @@ export class LogController {
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
||||
async create(@Body() logDto: LogDto) {
|
||||
try {
|
||||
return await this.logService.create(logDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
console.log(error)
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +87,7 @@ export class LogController {
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() logDto: LogDto
|
||||
): Promise<UpdateResult> {
|
||||
) {
|
||||
try {
|
||||
return await this.logService.update(id, logDto);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { PilotEntity } from "src/pilot/pilot.entity";
|
||||
|
||||
export class LogDto {
|
||||
pilotId: string;
|
||||
date: Date;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { TrackEntity } from 'src/track/track.entity';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { TrackEntity } from '../track/track.entity';
|
||||
import { ColumnNumericTransformer } from '../transformers/columnNumeric.transformer';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'logs' })
|
||||
@@ -25,52 +26,131 @@ export class LogEntity {
|
||||
@Column()
|
||||
routeTo: string;
|
||||
|
||||
@Column()
|
||||
@Column('numeric', {
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer()
|
||||
})
|
||||
durationOfFlight: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
singleEngineLand: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
simulatorAtd: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
landingsDay: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
landingsNight: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
groundTrainingReceived: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
flightTrainingReceived: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
crossCountry: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
night: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
solo: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
pilotInCommand: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentActual: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentSimulated: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentApproaches: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentHolds: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column('numeric', {
|
||||
nullable: true,
|
||||
precision: 10,
|
||||
scale: 1,
|
||||
transformer: new ColumnNumericTransformer(),
|
||||
})
|
||||
instrumentNavTrack: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
|
||||
@@ -15,23 +15,21 @@ export class LogInterceptor implements NestInterceptor {
|
||||
]);
|
||||
|
||||
return handler.handle().pipe(
|
||||
map((data: LogEntity[]) => {
|
||||
map((data: any) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const limitData = (data) => {
|
||||
return data.map((log: LogEntity) => {
|
||||
return {
|
||||
id: log.id,
|
||||
pilot: {
|
||||
name: log.pilot.name
|
||||
},
|
||||
date: log.date,
|
||||
aircraftMakeModel: log.aircraftMakeModel,
|
||||
routeFrom: log.routeFrom,
|
||||
routeTo: log.routeTo,
|
||||
durationOfFlight: log.durationOfFlight,
|
||||
tracks: log.tracks,
|
||||
};
|
||||
});
|
||||
const limitData = (log: LogEntity) => {
|
||||
return {
|
||||
id: log.id,
|
||||
pilot: {
|
||||
name: log.pilot.name
|
||||
},
|
||||
date: log.date,
|
||||
aircraftMakeModel: log.aircraftMakeModel,
|
||||
routeFrom: log.routeFrom,
|
||||
routeTo: log.routeTo,
|
||||
durationOfFlight: log.durationOfFlight,
|
||||
tracks: log.tracks,
|
||||
};
|
||||
}
|
||||
|
||||
if (req.headers.authorization) {
|
||||
@@ -41,17 +39,38 @@ export class LogInterceptor implements NestInterceptor {
|
||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
||||
|
||||
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
||||
const logs = limitData(data);
|
||||
if (data.entities) {
|
||||
const logs = data.entities.map((entity) => limitData(entity));
|
||||
|
||||
return logs;
|
||||
return {
|
||||
entities: logs,
|
||||
total: data.total,
|
||||
hasNextPage: data.hasNextPage
|
||||
};
|
||||
} else {
|
||||
const log = limitData(data);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
} else if (!req.headers.authorization && isPublic) {
|
||||
const publicData = limitData(data)
|
||||
const logs = publicData.slice(0, 5)
|
||||
if (data.entities) {
|
||||
const publicData = data.entities.map((entity) => limitData(entity))
|
||||
const logs = publicData.slice(0, 5)
|
||||
|
||||
return logs;
|
||||
return {
|
||||
entities: logs,
|
||||
total: logs.length,
|
||||
hasNextPage: false
|
||||
};
|
||||
} else {
|
||||
const publicData = limitData(data);
|
||||
|
||||
return publicData
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LogEntity } from './log.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PilotModule } from 'src/pilot/pilot.module';
|
||||
import { PilotModule } from '../pilot/pilot.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
366
api/src/log/log.service.spec.ts
Normal file
366
api/src/log/log.service.spec.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LogService } from './log.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { LogDto } from './log.dto';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PilotService } from '../pilot/pilot.service';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
|
||||
describe('LogService', () => {
|
||||
let service: LogService;
|
||||
|
||||
const mockFileService = {
|
||||
deleteFolder: jest.fn()
|
||||
}
|
||||
|
||||
const mockQueryBuilder = {
|
||||
createQueryBuilder: jest.fn().mockReturnThis(),
|
||||
innerJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn()
|
||||
}
|
||||
|
||||
const mockLogRepository = {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||
delete: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findOneBy: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}
|
||||
|
||||
const mockPilotRepository = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ConfigService,
|
||||
LogService,
|
||||
PilotService,
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: mockFileService
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(LogEntity),
|
||||
useValue: mockLogRepository
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(PilotEntity),
|
||||
useValue: mockPilotRepository
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
service = module.get<LogService>(LogService);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
})
|
||||
|
||||
it('create => should create a log entry', async () => {
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto
|
||||
|
||||
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
|
||||
|
||||
const result = await service.create(logDto);
|
||||
|
||||
expect(mockLogRepository.save).toHaveBeenCalled();
|
||||
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
|
||||
})
|
||||
|
||||
it('delete => should delete a log entry', async () => {
|
||||
const id: string = '';
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockFileService, 'deleteFolder').mockReturnValue(undefined);
|
||||
jest.spyOn(mockLogRepository, 'delete').mockReturnValue(log);
|
||||
|
||||
const result = await service.delete(id);
|
||||
|
||||
expect(result).toEqual(log);
|
||||
expect(mockLogRepository.delete).toHaveBeenCalled();
|
||||
expect(mockLogRepository.delete).toHaveBeenCalledWith({ id: id })
|
||||
})
|
||||
|
||||
it('find => should find a log entry by id', async () => {
|
||||
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockLogRepository, 'findOne').mockReturnValue(log);
|
||||
|
||||
const result = await service.find(id);
|
||||
|
||||
expect(result).toEqual(log);
|
||||
expect(mockLogRepository.findOne).toHaveBeenCalled();
|
||||
expect(mockLogRepository.findOne).toHaveBeenCalledWith({
|
||||
relations: [
|
||||
'pilot',
|
||||
'tracks'
|
||||
],
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('findLogsWithCount => should find log entries with count', async () => {
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log];
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockQueryBuilder, 'getManyAndCount').mockReturnValue([logs, count, morePages]);
|
||||
|
||||
const {entities, total, hasNextPage} = await service.findLogsWithCount();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(count).toEqual(total);
|
||||
expect(hasNextPage).toEqual(morePages);
|
||||
expect(mockQueryBuilder.getManyAndCount).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findLogsWithTracks => should find log entries with tracks', async () => {
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: [
|
||||
{
|
||||
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
} as LogEntity;
|
||||
const logs = [log];
|
||||
const count = 1
|
||||
const morePages = false
|
||||
|
||||
jest.spyOn(mockQueryBuilder, 'getManyAndCount').mockResolvedValue([logs, count, morePages])
|
||||
|
||||
const {entities, total, hasNextPage} = await service.findLogsWithTracks();
|
||||
|
||||
expect(entities).toEqual(logs);
|
||||
expect(count).toEqual(total);
|
||||
expect(hasNextPage).toEqual(morePages);
|
||||
expect(mockQueryBuilder.getManyAndCount).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('update => should update a log entry', async () => {
|
||||
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
|
||||
const logDto = {
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test'
|
||||
} as LogDto
|
||||
const log = {
|
||||
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
|
||||
jest.spyOn(mockLogRepository, 'findOneBy').mockReturnValue(log);
|
||||
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
|
||||
|
||||
const result = await service.update(id, logDto);
|
||||
|
||||
expect(result).toEqual(logDto);
|
||||
expect(mockLogRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockLogRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
expect(mockLogRepository.save).toHaveBeenCalled();
|
||||
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||
import { LogDto } from './log.dto';
|
||||
import { PilotService } from 'src/pilot/pilot.service';
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
import { FileService } from 'src/file/file.service';
|
||||
import { PilotService } from '../pilot/pilot.service';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { Logs } from './logs.interface';
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
@@ -17,42 +18,65 @@ export class LogService {
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<LogEntity> {
|
||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
||||
return await this.logRepository.findOne({
|
||||
where: { id: id },
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
|
||||
return logEntity;
|
||||
}
|
||||
|
||||
async findAll(): Promise<LogEntity[]> {
|
||||
return await this.logRepository.find({
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
}
|
||||
async findLogsWithCount(skip?: number, take?: number): Promise<Logs> {
|
||||
// const [entities, total] = await this.logRepository.findAndCount({
|
||||
// take,
|
||||
// skip,
|
||||
// order: {
|
||||
// date: 'DESC'
|
||||
// },
|
||||
// relations: ['pilot', 'tracks']
|
||||
// })
|
||||
|
||||
async create(logDto: LogDto): Promise<InsertResult> {
|
||||
try{
|
||||
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
|
||||
const [entities, total] = await this.logRepository
|
||||
.createQueryBuilder('logs')
|
||||
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||
.orderBy('logs.date', 'DESC')
|
||||
.skip(skip)
|
||||
.take(take)
|
||||
.getManyAndCount()
|
||||
|
||||
if (pilotEntity) {
|
||||
const { pilotId, ...newLogDto } = logDto;
|
||||
const log = this.logRepository.create({
|
||||
...newLogDto,
|
||||
pilot: pilotEntity
|
||||
})
|
||||
|
||||
return this.logRepository.insert(log);
|
||||
} else {
|
||||
throw new CustomError('Pilot not found', 'Not found', 404);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
return {
|
||||
entities,
|
||||
total,
|
||||
hasNextPage: skip + take < total
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, log: LogDto): Promise<UpdateResult> {
|
||||
return await this.logRepository.update(id, log);
|
||||
async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
|
||||
const [entities, total] = await this.logRepository
|
||||
.createQueryBuilder('logs')
|
||||
.innerJoinAndSelect('logs.tracks', 'track')
|
||||
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||
.orderBy('logs.date', 'DESC')
|
||||
.skip(skip)
|
||||
.take(take)
|
||||
.getManyAndCount();
|
||||
|
||||
return {
|
||||
entities,
|
||||
total,
|
||||
hasNextPage: skip + take < total
|
||||
}
|
||||
}
|
||||
|
||||
async create(logDto: LogDto): Promise<LogDto> {
|
||||
return this.logRepository.save(logDto);
|
||||
}
|
||||
|
||||
async update(id: string, logDto: LogDto): Promise<LogDto> {
|
||||
const logEntity: LogEntity = await this.logRepository.findOneBy({ id });
|
||||
const logEntityUpdated = Object.assign(logEntity, logDto)
|
||||
|
||||
delete logEntityUpdated.tracks;
|
||||
|
||||
return await this.logRepository.save(logEntityUpdated);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
|
||||
7
api/src/log/logs.interface.ts
Normal file
7
api/src/log/logs.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
|
||||
export interface Logs {
|
||||
entities: LogEntity[],
|
||||
total: number,
|
||||
hasNextPage: boolean
|
||||
}
|
||||
@@ -9,11 +9,7 @@ async function bootstrap() {
|
||||
const httpService = new HttpService();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors({
|
||||
origin: 'http://localhost:8080',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
credentials: true,
|
||||
});
|
||||
app.enableCors();
|
||||
app.setGlobalPrefix('api');
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.use(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'medical' })
|
||||
|
||||
260
api/src/pilot/pilot.controller.spec.ts
Normal file
260
api/src/pilot/pilot.controller.spec.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PilotController } from "./pilot.controller";
|
||||
import { PilotService } from './pilot.service';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { HttpException } from '@nestjs/common';
|
||||
|
||||
describe('PilotController', () => {
|
||||
let controller: PilotController;
|
||||
|
||||
const mockPilotService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PilotController],
|
||||
providers: [
|
||||
{
|
||||
provide: PilotService,
|
||||
useValue: mockPilotService
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PilotController>(PilotController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('find => should find a pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
|
||||
jest.spyOn(mockPilotService, 'find').mockReturnValue(pilot);
|
||||
|
||||
const result = await controller.find(id);
|
||||
|
||||
expect(result).toEqual(pilot);
|
||||
expect(mockPilotService.find).toHaveBeenCalled();
|
||||
expect(mockPilotService.find).toHaveBeenCalledWith(id)
|
||||
})
|
||||
|
||||
|
||||
it('find => should fail to find a pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
|
||||
jest.spyOn(mockPilotService, 'find').mockRejectedValue(new Error('Pilot not found'));
|
||||
|
||||
try {
|
||||
await controller.find(id);
|
||||
|
||||
fail('find did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.find).toHaveBeenCalled();
|
||||
expect(mockPilotService.find).toHaveBeenCalledWith(id);
|
||||
expect(mockPilotService.find).rejects.toThrow('Pilot not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('findAll => should find all pilots', async () => {
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
const pilots = [pilot];
|
||||
|
||||
jest.spyOn(mockPilotService, 'findAll').mockReturnValue(pilots);
|
||||
|
||||
const result = await controller.findAll();
|
||||
|
||||
expect(result).toEqual(pilots);
|
||||
expect(mockPilotService.findAll).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
it('findAll => should fail to find all pilots', async () => {
|
||||
jest.spyOn(mockPilotService, 'findAll').mockRejectedValue(new Error('Pilots not found'));
|
||||
|
||||
try {
|
||||
await controller.findAll();
|
||||
|
||||
fail('findAll did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.findAll).toHaveBeenCalled();
|
||||
expect(mockPilotService.findAll).rejects.toThrow('Pilots not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('create => should create a new pilot', async () => {
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotService, 'create').mockReturnValue(pilot);
|
||||
|
||||
const result = await controller.create(pilotDto);
|
||||
|
||||
expect(mockPilotService.create).toHaveBeenCalled();
|
||||
expect(mockPilotService.create).toHaveBeenCalledWith(pilotDto)
|
||||
expect(result).toEqual(pilot);
|
||||
})
|
||||
|
||||
it('create => should fail to create a new pilot', async () => {
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
jest.spyOn(mockPilotService, 'create').mockRejectedValue(new Error('Pilot failed to create'));
|
||||
|
||||
try {
|
||||
await controller.create(pilotDto);
|
||||
|
||||
fail('create did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.create).toHaveBeenCalled();
|
||||
expect(mockPilotService.create).toHaveBeenCalledWith(pilotDto);
|
||||
expect(mockPilotService.create).rejects.toThrow('Pilot failed to create')
|
||||
}
|
||||
})
|
||||
|
||||
it('update => should update an existing pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilotDto = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
jest.spyOn(mockPilotService, 'update').mockReturnValue(pilotDto);
|
||||
|
||||
const result = await controller.update(id, pilotDto);
|
||||
|
||||
expect(result).toEqual(pilotDto);
|
||||
expect(mockPilotService.update).toHaveBeenCalled();
|
||||
expect(mockPilotService.update).toHaveBeenCalledWith(id, pilotDto)
|
||||
})
|
||||
|
||||
it('update => should fail to update an existing pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilotDto = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
jest.spyOn(mockPilotService, 'update').mockRejectedValue(new Error('Pilot failed to update'))
|
||||
|
||||
try {
|
||||
await controller.update(id, pilotDto);
|
||||
|
||||
fail('update function did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.update).toHaveBeenCalled();
|
||||
expect(mockPilotService.update).toHaveBeenCalledWith(id, pilotDto);
|
||||
expect(mockPilotService.update).rejects.toThrow('Pilot failed to update')
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete an existing pilot', async () => {
|
||||
const id = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
|
||||
|
||||
jest.spyOn(mockPilotService, 'delete');
|
||||
|
||||
const result = await controller.delete(id);
|
||||
|
||||
expect(mockPilotService.delete).toHaveBeenCalled();
|
||||
expect(mockPilotService.delete).toHaveBeenCalledWith(id);
|
||||
})
|
||||
|
||||
it('delete => should fail to delete an existing pilot', async () => {
|
||||
const id = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
|
||||
|
||||
jest.spyOn(mockPilotService, 'delete').mockRejectedValue(new Error('Pilot failed to delete'));
|
||||
|
||||
try {
|
||||
await controller.delete(id);
|
||||
|
||||
fail('delete function did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(mockPilotService.delete).toHaveBeenCalled();
|
||||
expect(mockPilotService.delete).toHaveBeenCalledWith(id);
|
||||
expect(mockPilotService.delete).rejects.toThrow('Pilot failed to delete')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -50,9 +50,8 @@ export class PilotController {
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() pilotDto: PilotDto) {
|
||||
async create(@Body() pilotDto: PilotDto): Promise<PilotDto> {
|
||||
try {
|
||||
|
||||
return await this.pilotService.create(pilotDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
|
||||
export class PilotDto {
|
||||
name: string;
|
||||
address: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LogEntity } from 'src/log/log.entity';
|
||||
import { LogEntity } from '../log/log.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||
import { CertificateEntity } from '../certificate/certificate.entity';
|
||||
import { EndorsementEntity } from 'src/endorsement/endorsement.entity';
|
||||
import { MedicalEntity } from 'src/medical/medical.entity';
|
||||
import { EndorsementEntity } from '../endorsement/endorsement.entity';
|
||||
import { MedicalEntity } from '../medical/medical.entity';
|
||||
|
||||
@Entity({ name: 'pilots' })
|
||||
export class PilotEntity {
|
||||
|
||||
206
api/src/pilot/pilot.service.spec.ts
Normal file
206
api/src/pilot/pilot.service.spec.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PilotService } from './pilot.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { CustomError } from '../error/customError';
|
||||
|
||||
describe('PilotService', () => {
|
||||
let service: PilotService;
|
||||
|
||||
const mockPilotRepository = {
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOneBy: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PilotService,
|
||||
{
|
||||
provide: getRepositoryToken(PilotEntity),
|
||||
useValue: mockPilotRepository
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
service = module.get<PilotService>(PilotService);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
})
|
||||
|
||||
it('find => should find one pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(pilot);
|
||||
|
||||
const result = await service.find(id);
|
||||
|
||||
expect(result).toEqual(pilot);
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
})
|
||||
|
||||
it('find => should fail to find one pilot by id', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
|
||||
const mockCustomError = new CustomError('Pilot not found', 'Not found', 404)
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(undefined);
|
||||
|
||||
try {
|
||||
await service.find(id);
|
||||
|
||||
fail('find did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CustomError);
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
}
|
||||
})
|
||||
|
||||
it('findAll => should find all pilots', async () => {
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity;
|
||||
const pilots = [pilot];
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'find').mockReturnValue(pilots);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual(pilots);
|
||||
expect(mockPilotRepository.find).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('create => should create a new pilot', async () => {
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'save').mockReturnValue(pilot);
|
||||
|
||||
const result = await service.create(pilotDto);
|
||||
|
||||
expect(mockPilotRepository.save).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.save).toHaveBeenCalledWith(pilotDto);
|
||||
})
|
||||
|
||||
it('update => should update a pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilotDto = {
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234'
|
||||
} as PilotDto
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(pilot)
|
||||
jest.spyOn(mockPilotRepository, 'save').mockReturnValue(pilotDto);
|
||||
|
||||
const result = await service.update(id, pilotDto)
|
||||
|
||||
expect(result).toEqual(pilotDto);
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
|
||||
expect(mockPilotRepository.save).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.save).toHaveBeenCalledWith(pilotDto)
|
||||
})
|
||||
|
||||
it('delete => should delete a pilot', async () => {
|
||||
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
|
||||
const pilot = {
|
||||
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
|
||||
name: 'Test pilot',
|
||||
address: '123 Any Street',
|
||||
city: 'Any Town',
|
||||
state: 'Minnesota',
|
||||
postalCode: '55123',
|
||||
email: 'user@example.com',
|
||||
phone: '555-555-1234',
|
||||
userId: 'user@example.com',
|
||||
logs: [],
|
||||
certificates: [],
|
||||
endorsements: [],
|
||||
medical: []
|
||||
} as PilotEntity
|
||||
|
||||
jest.spyOn(mockPilotRepository, 'delete').mockReturnValue(pilot);
|
||||
|
||||
const result = await service.delete(id);
|
||||
|
||||
expect(result).toEqual(pilot);
|
||||
expect(mockPilotRepository.delete).toHaveBeenCalled();
|
||||
expect(mockPilotRepository.delete).toHaveBeenCalledWith({ id: id });
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { PilotEntity } from './pilot.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
import { CustomError } from '../error/customError';
|
||||
|
||||
@Injectable()
|
||||
export class PilotService {
|
||||
@@ -13,7 +13,7 @@ export class PilotService {
|
||||
|
||||
async find(id: string): Promise<PilotEntity> {
|
||||
try {
|
||||
const pilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||
|
||||
if (pilotEntity) {
|
||||
return pilotEntity
|
||||
@@ -29,15 +29,18 @@ export class PilotService {
|
||||
return await this.pilotRepository.find();
|
||||
}
|
||||
|
||||
async create(pilot: PilotDto): Promise<InsertResult> {
|
||||
return await this.pilotRepository.insert(pilot);
|
||||
async create(pilot: PilotDto): Promise<PilotDto> {
|
||||
return await this.pilotRepository.save(pilot);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
pilot: PilotDto
|
||||
): Promise<UpdateResult> {
|
||||
return await this.pilotRepository.update(id, pilot);
|
||||
pilotDto: PilotDto
|
||||
): Promise<PilotDto> {
|
||||
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id })
|
||||
const pilotEntityUpdated = Object.assign(pilotEntity, pilotDto)
|
||||
|
||||
return await this.pilotRepository.save(pilotEntityUpdated);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
|
||||
218
api/src/track/track.controller.spec.ts
Normal file
218
api/src/track/track.controller.spec.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TrackController } from './track.controller';
|
||||
import { TrackService } from './track.service';
|
||||
import { TrackDto } from './track.dto';
|
||||
import { TrackEntity } from './track.entity';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { HttpException } from '@nestjs/common';
|
||||
import { Readable } from 'stream';
|
||||
import { DeleteResult, InsertResult } from 'typeorm';
|
||||
|
||||
describe('TrackController', () => {
|
||||
let controller: TrackController;
|
||||
|
||||
const mockTrackService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
downloadTrackFile: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TrackController],
|
||||
providers: [
|
||||
ConfigService,
|
||||
FileService,
|
||||
{
|
||||
provide: TrackService,
|
||||
useValue: mockTrackService
|
||||
}
|
||||
]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<TrackController>(TrackController);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
})
|
||||
|
||||
it('findAll => should find all tracks by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity
|
||||
const tracks = [track]
|
||||
|
||||
jest.spyOn(mockTrackService, 'findAll').mockReturnValue(tracks);
|
||||
|
||||
const result = await controller.findAll(logId);
|
||||
|
||||
expect(result).toEqual(tracks);
|
||||
expect(mockTrackService.findAll).toHaveBeenCalled();
|
||||
expect(mockTrackService.findAll).toHaveBeenLastCalledWith(logId);
|
||||
})
|
||||
|
||||
it('findAll => should fail to find all tracks by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
|
||||
jest.spyOn(mockTrackService, 'findAll').mockRejectedValue(new Error('Tracks not found'))
|
||||
|
||||
try {
|
||||
await controller.findAll(logId);
|
||||
|
||||
fail('findAll did not throw error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.findAll).toHaveBeenCalled();
|
||||
expect(mockTrackService.findAll).rejects.toThrow('Tracks not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('create => create a track by a log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
const mockInsertResult: InsertResult = {
|
||||
identifiers: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
}
|
||||
],
|
||||
generatedMaps: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
createdAd: new Date()
|
||||
}
|
||||
],
|
||||
raw: {
|
||||
affectedRows: 1
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackService, 'create').mockReturnValue(mockInsertResult);
|
||||
|
||||
const result = await controller.create(logId, order, file);
|
||||
|
||||
expect(result).toEqual(mockInsertResult);
|
||||
expect(mockTrackService.create).toHaveBeenCalled();
|
||||
expect(mockTrackService.create).toHaveBeenCalledWith(logId, order, file)
|
||||
})
|
||||
|
||||
it('create => should fail to create a track by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackService, 'create').mockRejectedValue(new Error('Track failed to create'))
|
||||
|
||||
try {
|
||||
await controller.create(logId, order, file)
|
||||
|
||||
fail('create did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.create).toHaveBeenCalled();
|
||||
expect(mockTrackService.create).toHaveBeenCalledWith(logId, order, file);
|
||||
expect(mockTrackService.create).rejects.toThrow('Track failed to create')
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete a track by log id', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
const mockDeleteResult: DeleteResult ={
|
||||
raw: [],
|
||||
affected: 1
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackService, 'delete').mockReturnValue(mockDeleteResult);
|
||||
|
||||
const result = await controller.delete(id, fileName, logId);
|
||||
|
||||
expect(result).toEqual(mockDeleteResult);
|
||||
expect(mockTrackService.delete).toHaveBeenCalled();
|
||||
expect(mockTrackService.delete).toHaveBeenCalledWith(id, logId, fileName);
|
||||
|
||||
})
|
||||
|
||||
it('delete => should fail to delete a track by log id', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
|
||||
jest.spyOn(mockTrackService, 'delete').mockRejectedValue(new Error('Track failed to delete'));
|
||||
|
||||
try {
|
||||
await controller.delete(id, fileName, logId);
|
||||
|
||||
fail('delete did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.delete).toHaveBeenCalled();
|
||||
expect(mockTrackService.delete).toHaveBeenCalledWith(id, logId, fileName);
|
||||
expect(mockTrackService.delete).rejects.toThrow('Track failed to delete')
|
||||
}
|
||||
})
|
||||
|
||||
it('downloadTrack => should download a track by log id', async () => {
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
const mockStreamToBufferString = 'fake-stream-to-buffer-string'
|
||||
|
||||
jest.spyOn(mockTrackService, 'downloadTrackFile').mockReturnValue(mockStreamToBufferString);
|
||||
|
||||
const result = await controller.downloadTrack(logId, fileName);
|
||||
|
||||
expect(result).toEqual(mockStreamToBufferString);
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalled();
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalledWith(logId, fileName)
|
||||
})
|
||||
|
||||
it('downloadTrack => should fail to download a track by log id', async () => {
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
|
||||
jest.spyOn(mockTrackService, 'downloadTrackFile').mockRejectedValue(new Error('Track failed to download'));
|
||||
|
||||
try {
|
||||
await controller.downloadTrack(logId, fileName);
|
||||
|
||||
fail('downloadTrack did not throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalled();
|
||||
expect(mockTrackService.downloadTrackFile).toHaveBeenCalledWith(logId, fileName);
|
||||
expect(mockTrackService.downloadTrackFile).rejects.toThrow('Track failed to download')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogEntity } from 'src/log/log.entity';
|
||||
import { LogEntity } from '../log/log.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tracks' })
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TrackEntity } from "./track.entity";
|
||||
import { TrackController } from "./track.controller";
|
||||
import { FileService } from "../file/file.service";
|
||||
import { TrackService } from './track.service';
|
||||
import { LogModule } from 'src/log/log.module';
|
||||
import { LogModule } from '../log/log.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
346
api/src/track/track.service.spec.ts
Normal file
346
api/src/track/track.service.spec.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TrackService } from './track.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { TrackEntity } from './track.entity'
|
||||
import { TrackDto } from './track.dto';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { LogService } from '../log/log.service';
|
||||
import { LogEntity } from '../log/log.entity';
|
||||
import { PilotService } from '../pilot/pilot.service';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
import { Readable } from 'stream';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { CustomError } from '../error/customError';
|
||||
|
||||
describe('TrackService', () => {
|
||||
let service: TrackService;
|
||||
|
||||
const mockFileService = {
|
||||
deleteFile: jest.fn(),
|
||||
downloadFile: jest.fn(),
|
||||
uploadFile: jest.fn()
|
||||
}
|
||||
|
||||
const mockLogService = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
const mockPilotRepository = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
const mockTrackRepository = {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
downloadTrackFile: jest.fn(),
|
||||
findOneBy: jest.fn(),
|
||||
find: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ConfigService,
|
||||
LogService,
|
||||
PilotService,
|
||||
TrackService,
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: mockFileService
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mockLogService
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(PilotEntity),
|
||||
useValue: mockPilotRepository
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(TrackEntity),
|
||||
useValue: mockTrackRepository
|
||||
}
|
||||
]
|
||||
}).compile()
|
||||
|
||||
service = module.get<TrackService>(TrackService);
|
||||
})
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
})
|
||||
|
||||
it('create => should upload a track file', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const log = {
|
||||
id: '094ec69c-72c4-4995-8821-79d5b79bedda',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
const mockInsertResult: InsertResult = {
|
||||
identifiers: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde'
|
||||
}
|
||||
],
|
||||
generatedMaps: [
|
||||
{
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
createdAd: new Date()
|
||||
}
|
||||
],
|
||||
raw: {
|
||||
affectedRows: 1
|
||||
}
|
||||
}
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(log)
|
||||
jest.spyOn(mockTrackRepository, 'create').mockReturnValue(track)
|
||||
jest.spyOn(mockTrackRepository, 'insert').mockReturnValue(mockInsertResult);
|
||||
|
||||
const result = await service.create(logId, order, file);
|
||||
|
||||
expect(result).toEqual(mockInsertResult)
|
||||
expect(mockTrackRepository.insert).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.insert).toHaveBeenCalledWith({
|
||||
id: track.id,
|
||||
order: track.order,
|
||||
url: track.url
|
||||
})
|
||||
})
|
||||
|
||||
it('create => should fail to find log', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const order = 1;
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: 'test_track.kml',
|
||||
encoding: 'utf-8',
|
||||
mimetype: 'application/vnd.google-earth.kml+xml',
|
||||
size: 12345,
|
||||
destination: '/tmp/uploads',
|
||||
filename: 'unique-filename-123.kml',
|
||||
path: '/tmp/uploads/unique-filename-123.kml',
|
||||
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
|
||||
stream: new Readable()
|
||||
}
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(undefined);
|
||||
|
||||
try {
|
||||
await service.create(logId, order, file);
|
||||
|
||||
fail('find failed to throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CustomError);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(logId);
|
||||
}
|
||||
})
|
||||
|
||||
it('delete => should delete a track file', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const fileName = 'test_track.kml'
|
||||
const mockDeleteResult: DeleteResult ={
|
||||
raw: [],
|
||||
affected: 1
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackRepository, 'delete').mockReturnValue(mockDeleteResult);
|
||||
|
||||
const result = await service.delete(id, logId, fileName)
|
||||
|
||||
expect(result).toEqual(mockDeleteResult);
|
||||
expect(mockTrackRepository.delete).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.delete).toHaveBeenCalledWith({ id: id })
|
||||
})
|
||||
|
||||
it('delete => should fail to delete file in file service', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const fileName = 'test_track.kml'
|
||||
|
||||
jest.spyOn(mockFileService, 'deleteFile').mockRejectedValue(new Error('Failed to delete file'))
|
||||
|
||||
try {
|
||||
await service.delete(id, logId, fileName)
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(mockFileService.deleteFile).toHaveBeenCalled();
|
||||
expect(mockFileService.deleteFile).toHaveBeenCalledWith('tracks', logId, fileName)
|
||||
expect(mockFileService.deleteFile).rejects.toThrow('Failed to delete file')
|
||||
}
|
||||
})
|
||||
|
||||
it('downloadTrackFile => should download a track file by log id and filename', async () => {
|
||||
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
|
||||
const fileName = 'test_track.kml';
|
||||
const mockStreamToBufferString = 'fake-stream-to-buffer-string'
|
||||
|
||||
jest.spyOn(mockFileService, 'downloadFile').mockReturnValue(mockStreamToBufferString);
|
||||
|
||||
const result = await service.downloadTrackFile(logId, fileName);
|
||||
|
||||
expect(result).toEqual(mockStreamToBufferString);
|
||||
expect(mockFileService.downloadFile).toHaveBeenCalled();
|
||||
expect(mockFileService.downloadFile).toHaveBeenCalledWith('tracks', logId, fileName);
|
||||
})
|
||||
|
||||
it('find => should find a track file by id', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity
|
||||
|
||||
jest.spyOn(mockTrackRepository, 'findOneBy').mockReturnValue(track)
|
||||
|
||||
const result = await service.find(id);
|
||||
|
||||
expect(result).toEqual(track);
|
||||
expect(mockTrackRepository.findOneBy).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.findOneBy).toHaveBeenCalledWith({ id: id })
|
||||
})
|
||||
|
||||
it('findAll => should find all track files by log id', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
const log = {
|
||||
id: '094ec69c-72c4-4995-8821-79d5b79bedda',
|
||||
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
|
||||
date: new Date('2026-02-21'),
|
||||
aircraftMakeModel: 'Cessna 172M',
|
||||
aircraftIdentity: 'N12345',
|
||||
routeFrom: 'KMSP',
|
||||
routeTo: 'KMSP',
|
||||
durationOfFlight: 1,
|
||||
singleEngineLand: 1,
|
||||
simulatorAtd: null,
|
||||
landingsDay: 1,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: 1,
|
||||
night: null,
|
||||
solo: 1,
|
||||
pilotInCommand: 1,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: 'This is a test',
|
||||
tracks: []
|
||||
} as LogEntity;
|
||||
const track = {
|
||||
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
|
||||
url: 'http://track.example.com',
|
||||
order: 1
|
||||
} as TrackEntity;
|
||||
const tracks = [track]
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(log)
|
||||
jest.spyOn(mockTrackRepository, 'find').mockReturnValue(tracks);
|
||||
|
||||
const result = await service.findAll(logId);
|
||||
|
||||
expect(result).toEqual(tracks);
|
||||
expect(mockTrackRepository.find).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
log: {
|
||||
id: logId
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('findAll => should fail to find log', async () => {
|
||||
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
|
||||
|
||||
jest.spyOn(mockLogService, 'find').mockReturnValue(undefined);
|
||||
|
||||
try {
|
||||
await service.findAll(logId);
|
||||
|
||||
fail('find failed to throw error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CustomError);
|
||||
expect(mockLogService.find).toHaveBeenCalled();
|
||||
expect(mockLogService.find).toHaveBeenCalledWith(logId);
|
||||
}
|
||||
})
|
||||
|
||||
it('update => should update a track', async () => {
|
||||
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
|
||||
const track = {
|
||||
|
||||
} as TrackDto;
|
||||
const mockUpdateResult: UpdateResult = {
|
||||
affected: 1,
|
||||
raw: [],
|
||||
generatedMaps: []
|
||||
}
|
||||
|
||||
jest.spyOn(mockTrackRepository, 'update').mockReturnValue(mockUpdateResult)
|
||||
|
||||
const result = await service.update(id, track);
|
||||
|
||||
expect(result).toEqual(mockUpdateResult);
|
||||
expect(mockTrackRepository.update).toHaveBeenCalled();
|
||||
expect(mockTrackRepository.update).toHaveBeenCalledWith(id, track)
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,11 @@ import { TrackDto } from "./track.dto";
|
||||
import { TrackEntity } from "./track.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { LogService } from "src/log/log.service";
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
import { CustomError } from "src/error/customError";
|
||||
import { FileService } from "src/file/file.service";
|
||||
import { LogService } from "../log/log.service";
|
||||
import { LogEntity } from "../log/log.entity";
|
||||
import { CustomError } from "../error/customError";
|
||||
import { FileService } from "../file/file.service";
|
||||
import { Logs } from "src/log/logs.interface";
|
||||
|
||||
@Injectable()
|
||||
export class TrackService {
|
||||
@@ -19,11 +20,7 @@ export class TrackService {
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<TrackEntity> {
|
||||
try {
|
||||
return await this.trackRepository.findOneBy({ id });
|
||||
} catch (error) {
|
||||
throw new CustomError('Track not found', 'Not found', 404)
|
||||
}
|
||||
return await this.trackRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
async findAll(logId: string): Promise<TrackEntity[]> {
|
||||
@@ -40,10 +37,11 @@ export class TrackService {
|
||||
})
|
||||
|
||||
return tracks;
|
||||
} else {
|
||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,21 +67,11 @@ export class TrackService {
|
||||
}
|
||||
|
||||
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
||||
try {
|
||||
return await this.trackRepository.update(id, track);
|
||||
} catch(error) {
|
||||
throw error
|
||||
}
|
||||
return await this.trackRepository.update(id, track);
|
||||
}
|
||||
|
||||
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
||||
try {
|
||||
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||
|
||||
return downloadedFile;
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
return await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||
}
|
||||
|
||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
||||
|
||||
9
api/src/transformers/columnNumeric.transformer.ts
Normal file
9
api/src/transformers/columnNumeric.transformer.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class ColumnNumericTransformer {
|
||||
to(data: number): number {
|
||||
return data;
|
||||
}
|
||||
|
||||
from (data: string): number {
|
||||
return parseFloat(data);
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
BLAH BLAH
|
||||
...
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.1.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -14,7 +14,6 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@noahspan/noahspan-components": "^2.0.0-alpha-14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { IActionMenuProps } from './IActionMenuProps';
|
||||
import {
|
||||
IconButton,
|
||||
Icon,
|
||||
IconName,
|
||||
Dropdown
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
|
||||
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
||||
// const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
||||
// null
|
||||
// );
|
||||
// const auth = useAuth();
|
||||
|
||||
// const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
// setAnchorElAction(event.currentTarget);
|
||||
// };
|
||||
|
||||
// const onCloseActionMenu = () => {
|
||||
// setAnchorElAction(null);
|
||||
// };
|
||||
|
||||
const options = [
|
||||
'Item 1',
|
||||
'Item 2'
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
onOptionSelected={() => console.log('clicked!')}
|
||||
options={options}
|
||||
>
|
||||
<IconButton>
|
||||
<Icon className='text-2xl' iconName={IconName.ELLIPSIS_VERTICAL} />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</>
|
||||
// <div>
|
||||
// <IconButton onClick={onOpenActionMenu}>
|
||||
// <Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
||||
// </IconButton>
|
||||
// <Menu
|
||||
// anchorEl={anchorElAction}
|
||||
// keepMounted
|
||||
// open={Boolean(anchorElAction)}
|
||||
// onClose={onCloseActionMenu}
|
||||
// >
|
||||
// {auth.isAuthenticated &&
|
||||
// <>
|
||||
// <MenuItem onClick={() => onOpenCloseForm(FormMode.EDIT, id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.PEN} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>Edit</ListItemText>
|
||||
// </MenuItem>
|
||||
// {onOpenCloseTracks &&
|
||||
// <MenuItem onClick={() => onOpenCloseTracks!(FormMode.EDIT, id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.MAP_LOCATION_DOT} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>Tracks</ListItemText>
|
||||
// </MenuItem>
|
||||
// }
|
||||
// </>
|
||||
|
||||
// }
|
||||
// <MenuItem onClick={() => onOpenCloseForm(FormMode.VIEW, id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.EYE} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>View</ListItemText>
|
||||
// </MenuItem>
|
||||
// {auth.isAuthenticated &&
|
||||
// <>
|
||||
// <hr className="my-3" />
|
||||
// <MenuItem onClick={() => onDelete(id)}>
|
||||
// <ListItemIcon>
|
||||
// <Icon iconName={IconName.TRASH} size="lg" />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText>Delete</ListItemText>
|
||||
// </MenuItem>
|
||||
// </>
|
||||
// }
|
||||
// </Menu>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionMenu;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
|
||||
export interface IActionMenuProps {
|
||||
id: string;
|
||||
onDelete: (entryId: string) => void;
|
||||
onOpenCloseForm: (formMode: FormMode, id: string) => void;
|
||||
onOpenCloseTracks?: (formMode: FormMode, id: string) => void;
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
// import {
|
||||
// Button,
|
||||
// Dialog,
|
||||
// DialogActions,
|
||||
// DialogContent,
|
||||
// Icon,
|
||||
// IconName,
|
||||
// Loading
|
||||
// } from '@noahspan/noahspan-components';
|
||||
import { DialogConfirmationProps } from './ConfirmationDialogProps.interface';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@@ -3,34 +3,70 @@ import { useEffect, useReducer } from "react";
|
||||
import { useLogs } from "../../hooks/logs/UseLogs";
|
||||
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||
import { initialState, reducer } from "./reducer";
|
||||
import { useOidc } from "../../auth/oidcConfig";
|
||||
import Alert from "../alert/Alert";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||
import { ScreenSize } from "../../enums/screenSize";
|
||||
|
||||
const Flights = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { logs, logsLoading } = useLogs();
|
||||
const { isUserLoggedIn } = useOidc();
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
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(() => {
|
||||
|
||||
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: 'info', message: 'No flights found' }})
|
||||
}
|
||||
}, [logs])
|
||||
|
||||
getFlights(state.pageIndex, state.pageSize);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='max-w-screen-lg mx-auto'>
|
||||
<div className={`max-w-screen-lg mx-auto ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD? 'mr-4 ml-4' : ''}`}>
|
||||
<div className='prose mt-5 mb-5'>
|
||||
<h1>Flights</h1>
|
||||
</div>
|
||||
{!logsLoading && state.alert && (
|
||||
{!state.isLoading && state.alert && (
|
||||
<div>
|
||||
<Alert
|
||||
className='mb-5'
|
||||
@@ -43,34 +79,29 @@ const Flights = () => {
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{!logsLoading &&
|
||||
{!state.isLoading &&
|
||||
<div>
|
||||
<LogbookCard logs={state.flights} mode='flights' />
|
||||
<div>
|
||||
<LogbookCard logs={state.flights} mode='flights' />
|
||||
</div>
|
||||
{state.hasMoreFlights &&
|
||||
<div className='flex items-center justify-center mb-5'>
|
||||
<button className='btn btn-link' onClick={loadMore}>Load more</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{/* {logsLoading && [...Array(6)].map((_element, index) => {
|
||||
{state.isLoading && [...Array(5)].map((_element, index) => {
|
||||
return (
|
||||
<div className='mb-5'>
|
||||
<Card
|
||||
key={index}
|
||||
>
|
||||
<div className='p-5'>
|
||||
<div className='mb-2'><Skeleton height='h-[60px]' width='w-[200px]' /></div>
|
||||
<div className='mb-2'><Skeleton height='h-[30px]' width='w-[200px]' /></div>
|
||||
<div className='mb-2'><Skeleton height='h-[300px]' width='w-[300px]' /></div>
|
||||
{[...Array(4)].map((_element, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||
<div className='mb-2'><Skeleton height='h-[20px]' width='w-[300px]' /></div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||
<div className='card-body' key={index}>
|
||||
<div className='skeleton h-10 w-[150px]' />
|
||||
<div className='skeleton h-[350px]' />
|
||||
<div className='skeleton h-[65px]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})} */}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,4 +5,8 @@ export interface FlightsState {
|
||||
alert: Alert | undefined;
|
||||
flights: LogbookEntry[];
|
||||
isLoading: boolean;
|
||||
hasMoreFlights: boolean;
|
||||
pageIndex: number;
|
||||
pageSize: number
|
||||
totalFlights: number;
|
||||
}
|
||||
@@ -5,13 +5,17 @@ import { FlightsState } from "./FlightsState.interface";
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_FLIGHTS'; payload: LogbookEntry[] }
|
||||
| { type: 'SET_FLIGHTS'; payload: { flights: LogbookEntry[], hasMoreFlights: boolean, pageIndex: number, totalFlights: number } }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
|
||||
export const initialState: FlightsState = {
|
||||
alert: undefined,
|
||||
flights: [],
|
||||
isLoading: true
|
||||
isLoading: true,
|
||||
hasMoreFlights: true,
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
totalFlights: 0
|
||||
}
|
||||
|
||||
export const reducer = (
|
||||
@@ -28,7 +32,10 @@ export const reducer = (
|
||||
case 'SET_FLIGHTS': {
|
||||
return {
|
||||
...state,
|
||||
flights: action.payload
|
||||
flights: action.payload.flights,
|
||||
hasMoreFlights: action.payload.hasMoreFlights,
|
||||
pageIndex: action.payload.pageIndex,
|
||||
totalFlights: action.payload.totalFlights
|
||||
}
|
||||
}
|
||||
case 'SET_IS_LOADING': {
|
||||
|
||||
@@ -53,11 +53,16 @@ const LogForm = () => {
|
||||
if (pilots && FormMode.ADD) {
|
||||
const newPilotsOptions = pilots.map((pilot) => {
|
||||
return {
|
||||
key: pilot.id,
|
||||
label: pilot.name,
|
||||
value: pilot.id,
|
||||
};
|
||||
});
|
||||
|
||||
newPilotsOptions.unshift({
|
||||
label: '',
|
||||
value: '',
|
||||
})
|
||||
|
||||
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
||||
}
|
||||
}, [pilots]);
|
||||
@@ -80,9 +85,9 @@ const LogForm = () => {
|
||||
onChange={onChange}
|
||||
value={[value]}
|
||||
>
|
||||
{state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => {
|
||||
{state.pilotOptions?.map((pilotOption: { label: string, value: string; }) => {
|
||||
return (
|
||||
<option key={pilotOption.key}>{pilotOption.label}</option>
|
||||
<option value={pilotOption.value}>{pilotOption.label}</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
@@ -485,7 +490,9 @@ const LogForm = () => {
|
||||
type='number'
|
||||
className='input w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
min={0}
|
||||
onChange={(event) => onChange(Number(event.target.value))}
|
||||
step={0.1}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
@@ -505,6 +512,7 @@ const LogForm = () => {
|
||||
className='textarea w-full'
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
></textarea>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,6 @@ export interface LogFormState {
|
||||
alert: Alert | undefined;
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
pilotOptions: { key: string; label: string; }[];
|
||||
pilotOptions: { label: string; value: string; }[];
|
||||
selectedPilotName: string;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ type Action =
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] }
|
||||
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
||||
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
||||
|
||||
export const initialState: LogFormState = {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import { useEffect, useReducer, useState } from 'react';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { authColumns, unauthColumns } from './columns';
|
||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
import LogbookCard from '../logbookCard/LogbookCard';
|
||||
@@ -13,18 +12,40 @@ import { UserRole } from '../../enums/userRole';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { ScreenSize } from '../../enums/screenSize';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
|
||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table';
|
||||
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, HeaderContext, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
||||
import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
|
||||
import Alert from '../alert/Alert';
|
||||
import TrackMap from '../trackMap/TrackMap';
|
||||
|
||||
interface ActionsProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Logbook: React.FC<unknown> = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const logbookContext = useLogbookContext()
|
||||
const [columnVisibility, setColumnVisibility] = useState({});
|
||||
const logbookContext = useLogbookContext();
|
||||
const { isUserLoggedIn } = useOidc();
|
||||
const { userRole } = useUserRole();
|
||||
const { screenSize } = useBreakpoints();
|
||||
const Actions = ({ id }: ActionsProps) => {
|
||||
return (
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div tabIndex={0} role='button' className='btn btn-ghost p-0'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
||||
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-sm border border-base-300 !z-[100]">
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||
}
|
||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onDeleteLog(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||
const 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;
|
||||
@@ -51,7 +72,7 @@ const Logbook: React.FC<unknown> = () => {
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const date = new Date(info.getValue() as string);
|
||||
const date = new Date((info.getValue() as string).replace('Z', ''));
|
||||
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
|
||||
return formattedDate;
|
||||
@@ -116,32 +137,12 @@ const Logbook: React.FC<unknown> = () => {
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
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={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||
}
|
||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onDeleteLog(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<Actions id={info.row.original.id} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
||||
pilotName,
|
||||
date,
|
||||
aircraftMakeModel,
|
||||
route,
|
||||
durationOfFlight,
|
||||
notes
|
||||
]
|
||||
|
||||
const authColumns: ColumnDef<LogbookEntry>[] = [
|
||||
const columns: ColumnDef<LogbookEntry>[] = [
|
||||
pilotName,
|
||||
date,
|
||||
aircraftMakeModel,
|
||||
@@ -345,20 +346,76 @@ const Logbook: React.FC<unknown> = () => {
|
||||
},
|
||||
notes,
|
||||
actions
|
||||
]
|
||||
];
|
||||
|
||||
const getLogbookEntries = async () => {
|
||||
const onPaginationChange = (updater: any) => {
|
||||
const newPaginationState: PaginationState = typeof updater === 'function' ? updater(state.pagination) : updater;
|
||||
|
||||
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: state.entries,
|
||||
columns: columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: onPaginationChange,
|
||||
rowCount: state.totalEntries,
|
||||
state: {
|
||||
columnVisibility: columnVisibility,
|
||||
pagination: state.pagination
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
firstPage,
|
||||
getCanNextPage,
|
||||
getCanPreviousPage,
|
||||
getPageCount,
|
||||
getState,
|
||||
lastPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageIndex
|
||||
} = table;
|
||||
|
||||
const getLogbookEntries = async (pageIndex: number, pageSize: number) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||
|
||||
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
||||
const entries: LogbookEntry[] = response.data;
|
||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
const response: AxiosResponse = await httpClient.get(`api/logs`, {
|
||||
params: {
|
||||
skip: pageIndex * pageSize,
|
||||
take: pageSize
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.length > 0) {
|
||||
dispatch({ type: 'SET_ENTRIES', payload: response.data });
|
||||
if (response.data.entities.length > 0) {
|
||||
const entries: LogbookEntry[] = response.data.entities;
|
||||
const entryColumns: string[] = Object.keys(entries[0]);
|
||||
const columnVisibility: {[key: string]: boolean} = {}
|
||||
|
||||
if (state.alert) {
|
||||
for (const column of table.getAllLeafColumns()) {
|
||||
const entryColumnExists = entryColumns.find((entryColumn) => entryColumn === column.id);
|
||||
|
||||
if (entryColumnExists) {
|
||||
columnVisibility[column.id] = true
|
||||
} else {
|
||||
columnVisibility[column.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
columnVisibility['pilotName'] = true
|
||||
columnVisibility['actions'] = true
|
||||
|
||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
setColumnVisibility(columnVisibility)
|
||||
dispatch({ type: 'SET_ENTRIES', payload: { entries: entries, totalEntries: response.data.total }});
|
||||
|
||||
if (!isUserLoggedIn && response.data.entities.length >= 5) {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'A limited number of log entries displayed. Sign in to view all log entries.'}})
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined})
|
||||
}
|
||||
} else {
|
||||
@@ -421,7 +478,7 @@ const Logbook: React.FC<unknown> = () => {
|
||||
payload: false
|
||||
});
|
||||
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
||||
await getLogbookEntries();
|
||||
await getLogbookEntries(state.pagination?.pageIndex, state.pagination?.pageSize);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
@@ -441,66 +498,47 @@ const Logbook: React.FC<unknown> = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// let newColumns: ColumnDef<LogbookEntry>[];
|
||||
const onRowsPerPageChanged = (event: any) => {
|
||||
const newPaginationState: PaginationState = {
|
||||
pageIndex: 0,
|
||||
pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries
|
||||
};
|
||||
|
||||
// if (userRole === UserRole.WRITE) {
|
||||
// newColumns = [...authColumns];
|
||||
// } else {
|
||||
// newColumns = [...unauthColumns];
|
||||
// }
|
||||
|
||||
// const actionsColumnExists = newColumns.find((column) => column.id === 'actions');
|
||||
// const tracksColumnExists = newColumns.find((column) => column.id === 'actions');
|
||||
|
||||
// if (!actionsColumnExists) {
|
||||
// newColumns.push(actionsColumn);
|
||||
// }
|
||||
|
||||
// if (!tracksColumnExists) {
|
||||
// const notesColumnIndex = newColumns.findIndex((column) => column.id === 'notes')
|
||||
|
||||
// newColumns.splice(notesColumnIndex, 0, tracksColumn)
|
||||
// }
|
||||
|
||||
// dispatch({ type: 'SET_COLUMNS', payload: newColumns })
|
||||
// }, [isUserLoggedIn])
|
||||
|
||||
const table = useReactTable({
|
||||
data: state.entries,
|
||||
columns: authColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel()
|
||||
});
|
||||
|
||||
const textAlignment = {
|
||||
center: 'text-center',
|
||||
left: 'text-start',
|
||||
right: 'text-end'
|
||||
};
|
||||
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!logbookContext.state.isDrawerOpen) {
|
||||
getLogbookEntries();
|
||||
getLogbookEntries(state.pagination.pageIndex, state.pagination.pageSize);
|
||||
}
|
||||
}, [logbookContext.state.isDrawerOpen]);
|
||||
}, [logbookContext.state.isDrawerOpen, state.pagination.pageIndex, state.pagination.pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
const pages: number[] = [];
|
||||
|
||||
if (state.pagination?.pageSize) {
|
||||
const totalPages = getPageCount();
|
||||
|
||||
for(let i = 0; i < totalPages; i++) {
|
||||
pages.push(i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_PAGES', payload: pages })
|
||||
}, [state.totalEntries, state.pagination?.pageSize])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state.alert)
|
||||
}, [state.alert])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||
<div className='prose max-w-none col-span-10 mt-5 mb-5'>
|
||||
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
||||
<div className='prose max-w-none col-span-6 mt-5 mb-5'>
|
||||
<h1>Logbook</h1>
|
||||
</div>
|
||||
<div className='col-span-2 justify-self-end self-center'>
|
||||
<div className='col-span-6 justify-self-end self-center'>
|
||||
{userRole === UserRole.WRITE &&
|
||||
// <Button
|
||||
// color='primary'
|
||||
// onPress={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||
// startContent={<FontAwesomeIcon icon={faAdd} />}
|
||||
// data-testid="pilot-add-button"
|
||||
// >
|
||||
// Add Entry
|
||||
// </Button>
|
||||
<button className='btn btn-primary'
|
||||
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||
>
|
||||
@@ -516,117 +554,213 @@ const Logbook: React.FC<unknown> = () => {
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
severity={'info'}
|
||||
severity={state.alert.severity}
|
||||
>
|
||||
{state.alert.message}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{!state.isLoading && (
|
||||
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
|
||||
{state.entries.length > 0 && screenSize !== ScreenSize.SM && (
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table min-w-full h-auto table-auto w-full'>
|
||||
<thead className='bg-base-200'>
|
||||
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, headerIndex) => {
|
||||
return (
|
||||
<th
|
||||
className={`${header.column.columnDef.meta?.className ? header.column.columnDef.meta?.className : ''} group/th px-3 h-10 align-middle whitespace-nowrap text-foreground-500 text-tiny font-semibold ${headerGroupIndex === 0 ? 'first:rounded-tl-lg last:rounded-tr-lg' : ''} ${headerGroupIndex === table.getHeaderGroups().length - 1 ? 'first:rounded-bl-lg last:rounded-br-lg' : ''} data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||
colSpan={header.colSpan}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/* {header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
) : null} */}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD && (
|
||||
<div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg'>
|
||||
<div className='overflow-x-auto mb-5'>
|
||||
<div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'>
|
||||
<label className='select select-sm select-ghost'>
|
||||
<span className='label'>Rows per page</span>
|
||||
<select
|
||||
defaultValue={1}
|
||||
onChange={onRowsPerPageChanged}
|
||||
value={state.pagination?.pageSize}
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={state.totalEntries}>All</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
<table className='table min-w-full h-auto table-auto w-full'>
|
||||
<thead className='bg-base-200'>
|
||||
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, headerIndex) => {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td
|
||||
className={`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}
|
||||
>
|
||||
<div className={`${cell.column.columnDef.meta?.align ? cell.column.columnDef.meta?.align : ''}`}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
<th
|
||||
className={`${header.column.columnDef.meta?.className ? header.column.columnDef.meta?.className : ''} group/th px-3 h-10 align-middle whitespace-nowrap text-foreground-500 text-tiny font-semibold ${headerGroupIndex === 0 ? 'first:rounded-tl-lg last:rounded-tr-lg' : ''} ${headerGroupIndex === table.getHeaderGroups().length - 1 ? 'first:rounded-bl-lg last:rounded-br-lg' : ''} data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||
colSpan={header.colSpan}
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{/* {header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
) : null} */}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</tbody>
|
||||
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||
{table.getFooterGroups().map((footerGroup, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={footerGroup.id}>
|
||||
{footerGroup.headers.map((header) => {
|
||||
return (
|
||||
<td
|
||||
className='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}
|
||||
>
|
||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.footer,
|
||||
header.getContext()
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td
|
||||
className={`py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
|
||||
key={cell.id}
|
||||
>
|
||||
<div className={`${cell.column.columnDef.meta?.align ? cell.column.columnDef.meta?.align : ''}`}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
</table>
|
||||
</>
|
||||
</tbody>
|
||||
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||
{table.getFooterGroups().map((footerGroup, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={footerGroup.id}>
|
||||
{footerGroup.headers.map((header) => {
|
||||
return (
|
||||
<td
|
||||
className='roup/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
|
||||
key={header.id}
|
||||
>
|
||||
<div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.footer,
|
||||
header.getContext()
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
{state.pagination.pageSize !== state.totalEntries &&
|
||||
<div className='col-span-12 justify-self-center self-center'>
|
||||
<div className='join'>
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={firstPage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAnglesLeft} />
|
||||
</button>
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={previousPage}
|
||||
disabled={!getCanPreviousPage()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleLeft} />
|
||||
</button>
|
||||
{state.pages.map((page, index) => {
|
||||
return (
|
||||
<button
|
||||
className={`join-item btn btn-sm ${getState().pagination.pageIndex === index ? 'btn-active' : ''}`}
|
||||
onClick={() => setPageIndex(index)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={() => nextPage()}
|
||||
disabled={!getCanNextPage()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleRight} />
|
||||
</button>
|
||||
<button
|
||||
className='join-item btn btn-sm'
|
||||
onClick={lastPage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAnglesRight} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} />
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{/* {state.isLoading && !state.alert && (
|
||||
<>
|
||||
<div>
|
||||
<Loading size='xl' />
|
||||
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<div className='col-span-12'>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
const date = new Date(row.original.date.replace('Z', ''));
|
||||
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||
|
||||
return (
|
||||
<div className='card bg-base-100 border border-base-300 mb-5'>
|
||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
|
||||
<div className={`grid grid-cols-12 gap-3`}>
|
||||
<>
|
||||
<div className='col-span-8'>
|
||||
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||
</div>
|
||||
<div className='col-span-4 justify-self-end self-center'>
|
||||
<Actions id={row.original.id} />
|
||||
</div>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<>
|
||||
{cell.column.columnDef.header !== 'Actions' && cell.column.columnDef.header !== 'Date' && cell.column.columnDef.header !== 'Pilot' &&
|
||||
<>
|
||||
<div className='col-span-8 font-bold'>
|
||||
<span>{cell.getContext().column.columnDef.header?.toString()}</span>
|
||||
</div>
|
||||
<div className='col-span-4'>
|
||||
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
{state.isLoading && !state.alert && (
|
||||
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
|
||||
<div className='col-span-12 justify-self-center'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-12 justify-self-center'>
|
||||
Loading...
|
||||
</div>
|
||||
</>
|
||||
)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{logbookContext.state.isDrawerOpen && (
|
||||
<LogbookDrawer
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||
import { ColumnDef, PaginationState } from '@tanstack/react-table';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Alert } from '../../interfaces/Alert.interface';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
@@ -10,4 +10,7 @@ export interface LogbookState {
|
||||
isConfirmDialogLoading: boolean;
|
||||
isConfirmDialogOpen: boolean;
|
||||
isLoading: boolean;
|
||||
pages: number[];
|
||||
pagination: PaginationState;
|
||||
totalEntries: number;
|
||||
}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
import {
|
||||
CellContext,
|
||||
ColumnDef,
|
||||
HeaderContext,
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
|
||||
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||
const blah = info.table
|
||||
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
||||
let total: number = 0;
|
||||
|
||||
if (values.length > 0) {
|
||||
total = values.reduce((accumulator, currentValue) => accumulator + currentValue, total)
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
const pilotName: ColumnDef<LogbookEntry> = {
|
||||
id: 'pilotName',
|
||||
accessorKey: 'pilot',
|
||||
header: 'Pilot',
|
||||
footer: 'PAGE TOTALS',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const pilot: any = info.getValue();
|
||||
|
||||
return pilot.name
|
||||
}
|
||||
}
|
||||
const date: ColumnDef<LogbookEntry> = {
|
||||
id: 'date',
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const date = new Date(info.getValue() as string);
|
||||
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
const aircraftMakeModel: ColumnDef<LogbookEntry> = {
|
||||
id: 'aircraftMakeModel',
|
||||
accessorKey: 'aircraftMakeModel',
|
||||
header: 'Aircraft Make & Model'
|
||||
}
|
||||
const route: ColumnDef<LogbookEntry> = {
|
||||
id: 'route',
|
||||
header: 'Route of Flight',
|
||||
meta: {
|
||||
headerAlign: 'text-center',
|
||||
className: 'border-l border-base-400'
|
||||
},
|
||||
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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'landingsDay',
|
||||
accessorKey: 'landingsDay',
|
||||
header: 'Day',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||
meta: {
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'landingsNight',
|
||||
accessorKey: 'landingsNight',
|
||||
header: 'Night',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||
meta: {
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'instrument',
|
||||
header: 'Instrument',
|
||||
meta: {
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'instrumentActual',
|
||||
accessorKey: 'instrumentActual',
|
||||
header: 'Actual',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'instrumentHolds',
|
||||
accessorKey: 'instrumentHolds',
|
||||
header: 'Holds',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'instrumentNavTrack',
|
||||
accessorKey: 'instrumentNavTrack',
|
||||
header: 'Nav/Track',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'experienceTraining',
|
||||
header: 'Type of pilot experience or training',
|
||||
meta: {
|
||||
headerAlign: 'text-center'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'groundTrainingReceived',
|
||||
accessorKey: 'groundTrainingReceived',
|
||||
header: 'Ground Training Received',
|
||||
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||
meta: {
|
||||
align: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-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: 'text-right',
|
||||
headerAlign: 'text-right'
|
||||
},
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||
}
|
||||
]
|
||||
},
|
||||
notes
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||
import { ColumnDef, PaginationState } from '@tanstack/react-table';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Alert } from '../../interfaces/Alert.interface';
|
||||
import { LogbookEntry } from './LogbookEntry.interface';
|
||||
@@ -7,11 +7,12 @@ import { LogbookState } from './LogbookState.interface';
|
||||
type Action =
|
||||
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
|
||||
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
|
||||
| { type: 'SET_ENTRIES'; payload: { entries: LogbookEntry[], totalEntries: number } }
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean };
|
||||
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_PAGES'; payload: number[] }
|
||||
| { type: 'SET_PAGINATION'; payload: PaginationState };
|
||||
|
||||
export const initialState: LogbookState = {
|
||||
alert: undefined,
|
||||
@@ -19,7 +20,13 @@ export const initialState: LogbookState = {
|
||||
entries: [],
|
||||
isConfirmDialogLoading: false,
|
||||
isConfirmDialogOpen: false,
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
pages: [],
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 10
|
||||
},
|
||||
totalEntries: 0
|
||||
};
|
||||
|
||||
export const reducer = (
|
||||
@@ -42,7 +49,8 @@ export const reducer = (
|
||||
case 'SET_ENTRIES': {
|
||||
return {
|
||||
...state,
|
||||
entries: action.payload
|
||||
entries: action.payload.entries,
|
||||
totalEntries: action.payload.totalEntries
|
||||
};
|
||||
}
|
||||
case 'SET_ALERT': {
|
||||
@@ -63,6 +71,18 @@ export const reducer = (
|
||||
isLoading: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_PAGES': {
|
||||
return {
|
||||
...state,
|
||||
pages: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_PAGINATION': {
|
||||
return {
|
||||
...state,
|
||||
pagination: action.payload
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
||||
import TrackMap from "../trackMap/TrackMap";
|
||||
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||
import { ScreenSize } from "../../enums/screenSize";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{logs.map((log) => {
|
||||
const date = new Date(log.date);
|
||||
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
|
||||
const date = new Date(log.date.replace('Z', ''));
|
||||
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||
|
||||
return (
|
||||
<div className='card bg-base-100 border border-base-300 p-2'>
|
||||
<div className='card-body' key={log.id}>
|
||||
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-2' : ''}`} key={log.id}>
|
||||
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||
<div>
|
||||
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
||||
@@ -24,10 +29,10 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
||||
}
|
||||
</div>
|
||||
<div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
|
||||
<input type='radio' name='details-accordion' />
|
||||
<input type='checkbox' />
|
||||
<div className='collapse-title font-semibold'>Details</div>
|
||||
<div className='collapse-content'>
|
||||
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
|
||||
<div className={`grid grid-cols-12 gap-3 ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'mr-[5%] ml-[5%]' : 'mr-[30%] ml-[30%]'} mt-4 mb-4`}>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Aircraft Make and Model</span>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { LogbookDrawerProps } from "./LogbookDrawerProps.interface";
|
||||
import LogForm from "../logForm/LogForm";
|
||||
import TracksForm from "../tracksForm/TracksForm";
|
||||
@@ -9,7 +10,8 @@ import { FormMode } from "../../enums/formMode";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { AxiosError } from "axios";
|
||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||
import { useState } from "react";
|
||||
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||
import { ScreenSize } from "../../enums/screenSize";
|
||||
|
||||
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
const [activeTab, setActiveTab] = useState<string>('time');
|
||||
@@ -40,6 +42,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
};
|
||||
const methods = useForm();
|
||||
const logbookContext = useLogbookContext()
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
@@ -49,6 +52,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
|
||||
const onSubmit = async (data: unknown) => {
|
||||
try {
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: true });
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true });
|
||||
|
||||
if (!logbookContext.state.selectedLogId) {
|
||||
@@ -58,31 +62,26 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
}
|
||||
|
||||
methods.reset(defaultValues);
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
||||
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
|
||||
|
||||
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }});
|
||||
} finally {
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false });
|
||||
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
||||
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedKeyChanged = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
const onTabClicked = (event: any) => {
|
||||
setActiveTab(event.target.ariaLabel)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='drawer drawer-end'
|
||||
// closeButton={
|
||||
// <Button isIconOnly>
|
||||
// <FontAwesomeIcon icon={faXmark} />
|
||||
// </Button>
|
||||
// }
|
||||
// isOpen={logbookContext.state.isDrawerOpen}
|
||||
// onClose={onCancel}
|
||||
>
|
||||
<div className='drawer drawer-end'>
|
||||
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={logbookContext.state.isDrawerOpen} />
|
||||
<div className="drawer-side">
|
||||
<label
|
||||
@@ -90,12 +89,21 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
aria-label='close-sidebar'
|
||||
className='drawer-overlay'
|
||||
></label>
|
||||
<div className='menu bg-base-100 text-base-content min-h-full p-4' style={{ width: '25%' }}>
|
||||
<div className={`menu bg-base-100 text-base-content min-h-full p-4 ${screenSize === ScreenSize.SM ? 'w-full' : screenSize === ScreenSize.MD ? 'w-[66%]' : 'w-[25%]'}`}>
|
||||
<FormProvider {...methods}>
|
||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<h2>
|
||||
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||
</h2>
|
||||
<div className='grid grid-cols-12 mb-6'>
|
||||
<div className="col-span-10">
|
||||
<h2 className="mt-0 mb-0 self-center">
|
||||
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-span-2 justify-self-end self-center">
|
||||
<button className="btn btn-ghost" onClick={onCancel}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{logbookContext.state.formAlert && (
|
||||
<Alert
|
||||
className='mb-5'
|
||||
@@ -109,7 +117,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
)}
|
||||
<div className='tabs tabs-lift mb-5'>
|
||||
<label className='tab'>
|
||||
<input type='radio' name='my_tabs' defaultChecked={true} />
|
||||
<input aria-label='time' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'time' ? true : false} />
|
||||
<FontAwesomeIcon className='mr-1'icon={faClock} />
|
||||
Time
|
||||
</label>
|
||||
@@ -119,7 +127,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
{logbookContext.state.formMode !== FormMode.ADD &&
|
||||
<>
|
||||
<label className='tab'>
|
||||
<input type='radio' name='my_tabs'/>
|
||||
<input aria-label='tracks' type='radio' name='logbook_drawer_tabs' onChange={onTabClicked} checked={activeTab === 'tracks' ? true : false} />
|
||||
<FontAwesomeIcon className='mr-1' icon={faMapLocationDot} />
|
||||
Tracks
|
||||
</label>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Card, CardActions, CardBody, CardHeader} from "@noahspan/noahspan-components"
|
||||
import { PilotCardProps } from "./PilotCardProps.interface"
|
||||
import ActionMenu from "../actionMenu/ActionMenu"
|
||||
// import { Card, CardActions, CardBody, CardHeader} from "@noahspan/noahspan-components"
|
||||
// import { PilotCardProps } from "./PilotCardProps.interface"
|
||||
// import ActionMenu from "../actionMenu/ActionMenu"
|
||||
|
||||
const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
||||
return (
|
||||
<div>
|
||||
{pilots.map((pilot) => {
|
||||
return (
|
||||
<div>
|
||||
<Card key={pilot.id}>
|
||||
<CardBody>
|
||||
<CardHeader>{pilot.name}</CardHeader>
|
||||
<CardActions>
|
||||
<ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
||||
</CardActions>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// const PilotCard = ({ pilots, onDelete, onOpenCloseForm }: PilotCardProps) => {
|
||||
// return (
|
||||
// <div>
|
||||
// {pilots.map((pilot) => {
|
||||
// return (
|
||||
// <div>
|
||||
// <Card key={pilot.id}>
|
||||
// <CardBody>
|
||||
// <CardHeader>{pilot.name}</CardHeader>
|
||||
// <CardActions>
|
||||
// <ActionMenu id={pilot.id} onDelete={onDelete} onOpenCloseForm={onOpenCloseForm} />
|
||||
// </CardActions>
|
||||
// </CardBody>
|
||||
// </Card>
|
||||
// </div>
|
||||
// )
|
||||
// })}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
export default PilotCard;
|
||||
// export default PilotCard;
|
||||
@@ -4,14 +4,16 @@ import { IPilotFormProps } from './IPilotFormProps';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Person } from '@microsoft/microsoft-graph-types';
|
||||
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||
// import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||
// import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||
// import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
import httpClient from '../../httpClient/httpClient';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { states } from './states';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { ScreenSize } from '../../enums/screenSize';
|
||||
|
||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
pilotId,
|
||||
@@ -43,6 +45,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
});
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const { screenSize } = useBreakpoints();
|
||||
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
@@ -104,18 +107,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}, [pilotId]);
|
||||
|
||||
return (
|
||||
<div className='drawer drawer-end'
|
||||
// closeButton={
|
||||
// <Button isIconOnly>
|
||||
// <FontAwesomeIcon icon={faXmark} />
|
||||
// </Button>
|
||||
// }
|
||||
// isOpen={isDrawerOpen}
|
||||
// placement='right'
|
||||
// data-testid="pilot-drawer"
|
||||
// onClose={onCancel}
|
||||
// size='xl'
|
||||
>
|
||||
<div className='drawer drawer-end'>
|
||||
<input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={isDrawerOpen} />
|
||||
<div className='drawer-side'>
|
||||
<label
|
||||
@@ -123,13 +115,20 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
aria-label='close-sidebar'
|
||||
className='drawer-overlay'
|
||||
></label>
|
||||
<div className='menu bg-base-100 text-base-content min-h-full p-4' style={{ width: '25%' }}>
|
||||
<div className={`menu bg-base-100 text-base-content min-h-full p-4 ${screenSize === ScreenSize.SM ? 'w-full' : screenSize === ScreenSize.MD ? 'w-[66%]' : 'w-[25%]'}`}>
|
||||
<FormProvider {...methods}>
|
||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<h2>
|
||||
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||
</h2>
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className="col-span-10">
|
||||
<h2 className="mt-0 mb-0 self-center">
|
||||
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-span-2 justify-self-end self-center">
|
||||
<button className="btn btn-ghost" onClick={onCancel}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
<div className='col-span-3 self-center'>
|
||||
<span>Name *</span>
|
||||
</div>
|
||||
|
||||
@@ -1,153 +1,153 @@
|
||||
import { PilotFormCertificatesProps } from './PilotFormCertificatesProps.interface';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
Input,
|
||||
Select
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
// import { PilotFormCertificatesProps } from './PilotFormCertificatesProps.interface';
|
||||
// import {
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Icon,
|
||||
// IconButton,
|
||||
// IconName,
|
||||
// Input,
|
||||
// Select
|
||||
// } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
// import { FormMode } from '../../enums/formMode';
|
||||
|
||||
const PilotFormCertificates = ({
|
||||
isDisabled,
|
||||
mode
|
||||
}: PilotFormCertificatesProps ) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
// const PilotFormCertificates = ({
|
||||
// isDisabled,
|
||||
// mode
|
||||
// }: PilotFormCertificatesProps ) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// } = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'certificates',
|
||||
control
|
||||
});
|
||||
// const { fields, append, remove } = useFieldArray({
|
||||
// name: 'certificates',
|
||||
// control
|
||||
// });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
<div>
|
||||
<h5>Certificates</h5>
|
||||
</div>
|
||||
}
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h6>Type</h6>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Number</h6>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Date of Issue</h6>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Controller
|
||||
name={`certificates.${index}.type`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: 'Student',
|
||||
value: 'student'
|
||||
},
|
||||
{
|
||||
label: 'Private',
|
||||
value: 'private'
|
||||
},
|
||||
{
|
||||
label: 'Instrument',
|
||||
value: 'instrument'
|
||||
},
|
||||
{
|
||||
label: 'Recreational',
|
||||
value: 'recreational'
|
||||
},
|
||||
{
|
||||
label: 'Sport',
|
||||
value: 'sport'
|
||||
}
|
||||
]
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name={`certificates.${index}.number`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name={`certificates.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
disabled={isDisabled}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Icon iconName={IconName.TRASH} size='sm' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!isDisabled &&
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
number: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
startContent={<Icon iconName={IconName.PLUS} />}
|
||||
>
|
||||
Add Certificate
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <div>
|
||||
// {fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
// <div>
|
||||
// <h5>Certificates</h5>
|
||||
// </div>
|
||||
// }
|
||||
// {fields.length > 0 && (
|
||||
// <>
|
||||
// <div>
|
||||
// <h6>Type</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Number</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Date of Issue</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// </div>
|
||||
// {fields.map((field, index) => {
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`certificates.${index}.type`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Select
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// options={
|
||||
// [
|
||||
// {
|
||||
// label: 'Student',
|
||||
// value: 'student'
|
||||
// },
|
||||
// {
|
||||
// label: 'Private',
|
||||
// value: 'private'
|
||||
// },
|
||||
// {
|
||||
// label: 'Instrument',
|
||||
// value: 'instrument'
|
||||
// },
|
||||
// {
|
||||
// label: 'Recreational',
|
||||
// value: 'recreational'
|
||||
// },
|
||||
// {
|
||||
// label: 'Sport',
|
||||
// value: 'sport'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`certificates.${index}.number`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Input
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// )
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`certificates.${index}.dateOfIssue`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <IconButton
|
||||
// disabled={isDisabled}
|
||||
// onClick={() => remove(index)}
|
||||
// >
|
||||
// <Icon iconName={IconName.TRASH} size='sm' />
|
||||
// </IconButton>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// })}
|
||||
// </>
|
||||
// )}
|
||||
// {!isDisabled &&
|
||||
// <div>
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// append({
|
||||
// type: '',
|
||||
// number: '',
|
||||
// dateOfIssue: null
|
||||
// });
|
||||
// }}
|
||||
// startContent={<Icon iconName={IconName.PLUS} />}
|
||||
// >
|
||||
// Add Certificate
|
||||
// </Button>
|
||||
// </div>
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default PilotFormCertificates;
|
||||
// export default PilotFormCertificates;
|
||||
|
||||
@@ -1,129 +1,129 @@
|
||||
import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interface';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
Select
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
// import { PilotFormEndorsementsProps } from './PilotFormEndorsementsProps.interface';
|
||||
// import {
|
||||
// Button,
|
||||
// DatePicker,
|
||||
// Icon,
|
||||
// IconButton,
|
||||
// IconName,
|
||||
// Select
|
||||
// } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
// import { FormMode } from '../../enums/formMode';
|
||||
|
||||
const PilotFormEndorsements = ({
|
||||
mode,
|
||||
isDisabled
|
||||
}: PilotFormEndorsementsProps) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
// const PilotFormEndorsements = ({
|
||||
// mode,
|
||||
// isDisabled
|
||||
// }: PilotFormEndorsementsProps) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// setValue
|
||||
// } = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'endorsements',
|
||||
control
|
||||
});
|
||||
// const { fields, append, remove } = useFieldArray({
|
||||
// name: 'endorsements',
|
||||
// control
|
||||
// });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
<div>
|
||||
<h5>Endorsements</h5>
|
||||
</div>
|
||||
}
|
||||
{fields.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h6>Type</h6>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Date of Issue</h6>
|
||||
</div>
|
||||
<div></div>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Controller
|
||||
name={`endorsements.${index}.type`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: 'Complex',
|
||||
value: 'complex'
|
||||
},
|
||||
{
|
||||
label: 'High Performance',
|
||||
value: 'highPerfomance'
|
||||
},
|
||||
{
|
||||
label: 'High Altitude',
|
||||
value: 'highAltitude'
|
||||
},
|
||||
{
|
||||
label: 'Tailwheel',
|
||||
value: 'tailwheel'
|
||||
}
|
||||
]
|
||||
}
|
||||
value={value ? value : ''}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name={`endorsements.${index}.dateOfIssue`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
disabled={isDisabled}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Icon iconName={IconName.TRASH} size="sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!isDisabled &&
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
append({
|
||||
type: '',
|
||||
dateOfIssue: null
|
||||
});
|
||||
}}
|
||||
startContent={<Icon iconName={IconName.PLUS} />}
|
||||
>
|
||||
Add Endorsement
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <div>
|
||||
// {fields.length > 0 || mode !== FormMode.VIEW &&
|
||||
// <div>
|
||||
// <h5>Endorsements</h5>
|
||||
// </div>
|
||||
// }
|
||||
// {fields.length > 0 && (
|
||||
// <>
|
||||
// <div>
|
||||
// <h6>Type</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Date of Issue</h6>
|
||||
// </div>
|
||||
// <div></div>
|
||||
// {fields.map((field, index) => {
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`endorsements.${index}.type`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Select
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// options={
|
||||
// [
|
||||
// {
|
||||
// label: 'Complex',
|
||||
// value: 'complex'
|
||||
// },
|
||||
// {
|
||||
// label: 'High Performance',
|
||||
// value: 'highPerfomance'
|
||||
// },
|
||||
// {
|
||||
// label: 'High Altitude',
|
||||
// value: 'highAltitude'
|
||||
// },
|
||||
// {
|
||||
// label: 'Tailwheel',
|
||||
// value: 'tailwheel'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// value={value ? value : ''}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name={`endorsements.${index}.dateOfIssue`}
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <IconButton
|
||||
// disabled={isDisabled}
|
||||
// onClick={() => remove(index)}
|
||||
// >
|
||||
// <Icon iconName={IconName.TRASH} size="sm" />
|
||||
// </IconButton>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// })}
|
||||
// </>
|
||||
// )}
|
||||
// {!isDisabled &&
|
||||
// <div>
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// append({
|
||||
// type: '',
|
||||
// dateOfIssue: null
|
||||
// });
|
||||
// }}
|
||||
// startContent={<Icon iconName={IconName.PLUS} />}
|
||||
// >
|
||||
// Add Endorsement
|
||||
// </Button>
|
||||
// </div>
|
||||
// }
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default PilotFormEndorsements;
|
||||
// export default PilotFormEndorsements;
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { DatePicker, Select } from '@noahspan/noahspan-components';
|
||||
import { Controller, useFormContext } from "react-hook-form"
|
||||
import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
||||
// import { DatePicker, Select } from '@noahspan/noahspan-components';
|
||||
// import { Controller, useFormContext } from "react-hook-form"
|
||||
// import { PilotFormMedicalProps } from "./PilotFormMedicalProps.interface";
|
||||
|
||||
const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useFormContext();
|
||||
// const PilotFormMedical = ({ isDisabled }: PilotFormMedicalProps) => {
|
||||
// const {
|
||||
// control,
|
||||
// formState: { errors },
|
||||
// setValue
|
||||
// } = useFormContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h5>Medical</h5>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Class</h6>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="medicalClass"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
options={
|
||||
[
|
||||
{
|
||||
label: 'First',
|
||||
value: 'first'
|
||||
},
|
||||
{
|
||||
label: 'Second',
|
||||
value: 'second'
|
||||
},
|
||||
{
|
||||
label: 'Third',
|
||||
value: 'third'
|
||||
},
|
||||
{
|
||||
label: 'Basic Med',
|
||||
value: 'basicMed'
|
||||
}
|
||||
]
|
||||
}
|
||||
value={value ? value : ''}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Expiration</h6>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="medicalExpiration"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<DatePicker
|
||||
disabled={isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// return (
|
||||
// <div>
|
||||
// <div>
|
||||
// <h5>Medical</h5>
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Class</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="medicalClass"
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <Select
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// options={
|
||||
// [
|
||||
// {
|
||||
// label: 'First',
|
||||
// value: 'first'
|
||||
// },
|
||||
// {
|
||||
// label: 'Second',
|
||||
// value: 'second'
|
||||
// },
|
||||
// {
|
||||
// label: 'Third',
|
||||
// value: 'third'
|
||||
// },
|
||||
// {
|
||||
// label: 'Basic Med',
|
||||
// value: 'basicMed'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// value={value ? value : ''}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <h6>Expiration</h6>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Controller
|
||||
// name="medicalExpiration"
|
||||
// control={control}
|
||||
// render={({ field: { onChange, value } }) => {
|
||||
// return (
|
||||
// <DatePicker
|
||||
// disabled={isDisabled}
|
||||
// onChange={onChange}
|
||||
// value={value}
|
||||
// />
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
export default PilotFormMedical
|
||||
// export default PilotFormMedical
|
||||
@@ -4,7 +4,7 @@ import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||
import PilotCard from '../pilotCard/PilotCard';
|
||||
// import PilotCard from '../pilotCard/PilotCard';
|
||||
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||
import { UserRole } from '../../enums/userRole';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
@@ -17,11 +17,31 @@ import { Pilot } from './Pilot.interface';
|
||||
import Alert from '../alert/Alert';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
|
||||
interface ActionsProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Pilots: React.FC<unknown> = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { userRole } = useUserRole();
|
||||
const { screenSize } = useBreakpoints();
|
||||
const { isUserLoggedIn } = useOidc();
|
||||
const Actions = ({ id }: ActionsProps) => {
|
||||
return (
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
||||
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300">
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||
}
|
||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onDeletePilot(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getPilots = async () => {
|
||||
try {
|
||||
@@ -31,8 +51,6 @@ const Pilots: React.FC<unknown> = () => {
|
||||
`api/pilots`
|
||||
);
|
||||
|
||||
console.log(response)
|
||||
|
||||
if (response.data.length > 0) {
|
||||
dispatch({ type: 'SET_PILOTS', payload: response.data });
|
||||
|
||||
@@ -136,18 +154,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
},
|
||||
cell: (info: CellContext<Pilot, unknown>) => {
|
||||
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, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||
}
|
||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||
<li><a onClick={() => onDeletePilot(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<Actions id={info.row.original.id} />
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -174,12 +181,12 @@ const Pilots: React.FC<unknown> = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
||||
<div className='prose max-w-none col-span-10 mt-5 mb-5' >
|
||||
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
||||
<div className='prose max-w-none col-span-6 mt-5 mb-5' >
|
||||
<h1>Pilots</h1>
|
||||
</div>
|
||||
<div className='col-span-2 justify-self-end self-center'>
|
||||
{userRole === UserRole.WRITE &&
|
||||
<div className='col-span-6 justify-self-end self-center'>
|
||||
{!state.isLoading && userRole === UserRole.WRITE &&
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||
@@ -203,8 +210,8 @@ const Pilots: React.FC<unknown> = () => {
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className='col-span-12 bg-base-100 p-5 border border-base-100 rounded-lg'>
|
||||
{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 className='table min-w-full h-auto table-auto w-full'>
|
||||
<thead className='[&>tr]:first:rounded-lg bg-base-200'>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
@@ -259,11 +266,42 @@ const Pilots: React.FC<unknown> = () => {
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<div className='col-span-12'>
|
||||
<>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<div className='card bg-base-100 border border-base-300 mb-5'>
|
||||
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
|
||||
<div className={`grid grid-cols-12 gap-3`}>
|
||||
<>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<>
|
||||
{cell.column.columnDef.header !== 'Actions' &&
|
||||
<>
|
||||
<div className='col-span-10 self-center'>
|
||||
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<Actions id={row.original.id} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{state.isFormOpen && (
|
||||
<PilotForm
|
||||
|
||||
@@ -105,10 +105,6 @@ const SiteNav = () => {
|
||||
}
|
||||
}, [isUserLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(screenSize)
|
||||
}, [screenSize])
|
||||
|
||||
return (
|
||||
<div className="navbar bg-base-100 shadow-sm w-full">
|
||||
<div className={`navbar-start ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
|
||||
@@ -136,7 +132,7 @@ const SiteNav = () => {
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={`navbar-end ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
|
||||
<div className={`navbar-end ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'mr-8' : ''}`}>
|
||||
{!isUserLoggedIn &&
|
||||
<button className='btn btn-ghost' onClick={() => login()}><FontAwesomeIcon icon={faSignIn} />Sign In</button>
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ const TracksForm = () => {
|
||||
</>
|
||||
)
|
||||
})}
|
||||
{logbookContext.state.formMode === FormMode.ADD &&
|
||||
{logbookContext.state.formMode === FormMode.EDIT &&
|
||||
<div className='col-span-12'>
|
||||
<label
|
||||
className='btn cursor-pointer w-full'
|
||||
|
||||
@@ -15,7 +15,7 @@ httpClient.interceptors.request.use(async (config) => {
|
||||
|
||||
if (oidc.isUserLoggedIn) {
|
||||
const { accessToken } = await oidc.getTokens();
|
||||
console.log(accessToken)
|
||||
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
strictPort: true
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
dbs:
|
||||
- path: /var/lib/data/flying.db
|
||||
- path: /mnt/data/flying.db
|
||||
replicas:
|
||||
- path: /mnt/backup/flying.db
|
||||
- path: /mnt/data/backup/flying.db
|
||||
@@ -3,24 +3,39 @@ version: '3.8'
|
||||
services:
|
||||
azurite:
|
||||
container_name: azurite-flying
|
||||
image: mcr.microsoft.com/azure-storage/azurite
|
||||
image: mcr.microsoft.com/azure-storage/azurite
|
||||
ports:
|
||||
- '10000:10000'
|
||||
- '10001:10001'
|
||||
- '10002:10002'
|
||||
command: 'azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --loose --skipApiVersionCheck'
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "127.0.0.1", "10000"]
|
||||
volumes:
|
||||
- ./azurite-data:/data
|
||||
|
||||
seed:
|
||||
image: mcr.microsoft.com/azure-cli
|
||||
depends_on:
|
||||
azurite:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./seed-data:/data
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
az storage container create --name mycontainer --connection-string 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;' &&
|
||||
az storage blob upload-batch --destination mycontainer --source /data --connection-string 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;'
|
||||
"
|
||||
|
||||
restore:
|
||||
container_name: restore
|
||||
image: litestream/litestream:0.3.13
|
||||
volumes:
|
||||
- ./database/flying.db:/var/lib/data/flying.db
|
||||
- ./database/flying.db:/mnt/data/flying.db
|
||||
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
||||
- backup:/mnt/data/backup
|
||||
- data:/var/lib/data
|
||||
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /var/lib/data/flying.db
|
||||
- data:/mnt/data
|
||||
command: restore -config /mnt/litestream/litestream.yml -if-db-not-exists -if-replica-exists /mnt/data/flying.db
|
||||
|
||||
app:
|
||||
container_name: flying
|
||||
@@ -31,9 +46,9 @@ services:
|
||||
env_file:
|
||||
- ./api/.env
|
||||
environment:
|
||||
- DB_PATH=../../var/lib/data/flying.db
|
||||
- DB_PATH=../../mnt/data/flying.db
|
||||
volumes:
|
||||
- data:/var/lib/data
|
||||
- data:/mnt/data
|
||||
depends_on:
|
||||
restore:
|
||||
condition: service_completed_successfully
|
||||
@@ -44,13 +59,14 @@ services:
|
||||
volumes:
|
||||
- ./database/litestream/litestream.yml:/mnt/litestream/litestream.yml
|
||||
- backup:/mnt
|
||||
- data:/var/lib/data
|
||||
- data:/mnt/data
|
||||
command: replicate -config /mnt/litestream/litestream.yml
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_started
|
||||
|
||||
volumes:
|
||||
# azurite-data:
|
||||
azurite-data:
|
||||
backup:
|
||||
data:
|
||||
data:
|
||||
seed-data:
|
||||
3
infrastructure/.gitignore
vendored
3
infrastructure/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
# Terraform
|
||||
.terraform/*
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
*.tfstate.*
|
||||
.DS_Store
|
||||
1
infrastructure/backend/prod.backend.tfvars
Normal file
1
infrastructure/backend/prod.backend.tfvars
Normal file
@@ -0,0 +1 @@
|
||||
container_name = "tfstate-flying-prod"
|
||||
1
infrastructure/backend/test.backend.tfvars
Normal file
1
infrastructure/backend/test.backend.tfvars
Normal file
@@ -0,0 +1 @@
|
||||
container_name = "tfstate-flying-test"
|
||||
@@ -1,8 +1,3 @@
|
||||
# data "azuread_application" "app_registration" {
|
||||
# provider = azuread.external_tenant
|
||||
# display_name = module.environment.app_reg_name
|
||||
# }
|
||||
|
||||
resource "azurerm_container_app" "container_app" {
|
||||
name = module.environment.app_name
|
||||
container_app_environment_id = data.azurerm_container_app_environment.container_app_environment.id
|
||||
@@ -14,7 +9,7 @@ resource "azurerm_container_app" "container_app" {
|
||||
max_replicas = 2
|
||||
|
||||
init_container {
|
||||
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/data/flying.db"]
|
||||
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/mnt/data/flying.db"]
|
||||
cpu = 0.25
|
||||
image = "litestream/litestream:0.5.2"
|
||||
memory = "0.5Gi"
|
||||
@@ -22,12 +17,12 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
volume_mounts {
|
||||
name = "data"
|
||||
path = "/var/lib/data"
|
||||
path = "/mnt/data"
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
name = "backup"
|
||||
path = "/mnt/data"
|
||||
path = "/mnt/data/backup"
|
||||
sub_path = "data"
|
||||
}
|
||||
|
||||
@@ -47,12 +42,12 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
volume_mounts {
|
||||
name = "data"
|
||||
path = "/var/lib/data"
|
||||
path = "/mnt/data"
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
name = "backup"
|
||||
path = "/mnt/data"
|
||||
path = "/mnt/data/backup"
|
||||
sub_path = "data"
|
||||
}
|
||||
|
||||
@@ -65,7 +60,7 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
container {
|
||||
cpu = 0.25
|
||||
image = "noahspan/flying:19615036250"
|
||||
image = "noahspan/flying:20589367895"
|
||||
memory = "0.5Gi"
|
||||
name = "flying"
|
||||
|
||||
@@ -104,6 +99,16 @@ resource "azurerm_container_app" "container_app" {
|
||||
value = var.JWKS_URI
|
||||
}
|
||||
|
||||
env {
|
||||
name = "NODE_ENV"
|
||||
value = "test"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "SESSION_SECRET"
|
||||
secret_name = "session-secret"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "TENANT_ID"
|
||||
value = var.EXTERNAL_TENANT_ID
|
||||
@@ -111,18 +116,18 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
env {
|
||||
name = "DB_PATH"
|
||||
value = "/var/lib/data/flying.db"
|
||||
value = "/mnt/data/flying.db"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "DB_SYNC"
|
||||
value = "false"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
startup_probe {
|
||||
failure_count_threshold = 10
|
||||
initial_delay = 1
|
||||
interval_seconds = 2
|
||||
failure_count_threshold = 3
|
||||
initial_delay = 15
|
||||
interval_seconds = 30
|
||||
path = "/api/health"
|
||||
port = 3000
|
||||
transport = "HTTP"
|
||||
@@ -130,7 +135,7 @@ resource "azurerm_container_app" "container_app" {
|
||||
|
||||
volume_mounts {
|
||||
name = "data"
|
||||
path = "/var/lib/data"
|
||||
path = "/mnt/data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +184,11 @@ resource "azurerm_container_app" "container_app" {
|
||||
value = var.DOCKER_IO_PASSWORD
|
||||
}
|
||||
|
||||
secret {
|
||||
name = "session-secret"
|
||||
value = var.SESSION_SECRET
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [ template[0].container[0].image, template[0].container[0].image, template[0].init_container[0].image, registry[0].server ]
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ resource "azurerm_container_app_environment_storage" "container_app_environment_
|
||||
account_name = azurerm_storage_account.storage_account.name
|
||||
share_name = azurerm_storage_share.storage_share[0].name
|
||||
access_key = azurerm_storage_account.storage_account.primary_access_key
|
||||
access_mode = "ReadOnly"
|
||||
access_mode = "ReadWrite"
|
||||
}
|
||||
@@ -16,12 +16,9 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
backend "remote" {
|
||||
hostname = "app.terraform.io"
|
||||
organization = "noahspan"
|
||||
|
||||
workspaces {
|
||||
prefix = "noahspan-"
|
||||
}
|
||||
backend "azurerm" {
|
||||
resource_group_name = "noahspan"
|
||||
storage_account_name = "noahspanterraform"
|
||||
key = "terraform.tfstate"
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,11 @@ variable "EXTERNAL_TENANT_ID" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "SESSION_SECRET" {
|
||||
sensitive = true
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "TENANT_ID" {
|
||||
type = string
|
||||
}
|
||||
|
||||
463
package-lock.json
generated
463
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@noahspan/flying",
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@noahspan/flying",
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.0.2",
|
||||
"workspaces": [
|
||||
"api",
|
||||
"client",
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.0.2",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@azure/storage-blob": "^12.27.0",
|
||||
@@ -81,12 +81,11 @@
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.0.2",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@noahspan/noahspan-components": "^2.0.0-alpha-14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
@@ -159,71 +158,6 @@
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@noahspan/noahspan-components": {
|
||||
"version": "2.0.0-alpha-14",
|
||||
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-2.0.0-alpha-14.tgz",
|
||||
"integrity": "sha512-PlIQa3Np9pOaHFbLe0bIQTIe1jYVEWOTiSYAS3VXhO1MZ7NGlSu8vgBJQ9ireD8NueE+FlYi56n9cNgeDNY6IA==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"react-daisyui-timetools": "^2.0.2",
|
||||
"react-day-picker": "^9.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.x",
|
||||
"react-dom": "^19.x"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@noahspan/noahspan-components/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@noahspan/noahspan-components/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@noahspan/noahspan-components/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@noahspan/noahspan-components/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
|
||||
"integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
|
||||
"react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"client/node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
@@ -232,20 +166,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"client/node_modules/react-daisyui-timetools": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-daisyui-timetools/-/react-daisyui-timetools-2.0.2.tgz",
|
||||
"integrity": "sha512-ruNo9eUdNm/tlkgo+zd7qZbDgxTObRAH4CW8KBpxpWDTrS0krYuWcFcao9i4wwYeVfPnWrtgw5AsH895EmKgFA==",
|
||||
"peerDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"daisyui": "^5",
|
||||
"dayjs": "^1.11.13",
|
||||
"postcss": "^8.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.0.12"
|
||||
}
|
||||
},
|
||||
"client/node_modules/react-dom": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
@@ -257,18 +177,6 @@
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
@@ -2317,11 +2225,6 @@
|
||||
"google-protobuf": "^3.21.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@date-fns/tz": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
|
||||
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -2424,36 +2327,6 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
@@ -3270,6 +3143,7 @@
|
||||
"version": "2.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.40.0.tgz",
|
||||
"integrity": "sha512-1fcPVrB/NkbNcGNfCy+Cgnvwxt6/sbIEEFgZHFBJ670zYLegENYJF8qMo7x3LqBjWX2/Eneq5BVVRCLTmlJN+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nestjs/axios": {
|
||||
@@ -5104,282 +4978,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz",
|
||||
"integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.14",
|
||||
"@tailwindcss/oxide": "4.1.14",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz",
|
||||
"integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.0",
|
||||
"lightningcss": "1.30.1",
|
||||
"magic-string": "^0.30.19",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz",
|
||||
"integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==",
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.4",
|
||||
"tar": "^7.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.14",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.14",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.14",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.14",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.14",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.14",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz",
|
||||
"integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz",
|
||||
"integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz",
|
||||
"integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz",
|
||||
"integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz",
|
||||
"integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz",
|
||||
"integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz",
|
||||
"integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz",
|
||||
"integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz",
|
||||
"integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz",
|
||||
"integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.0.5",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz",
|
||||
"integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz",
|
||||
"integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
||||
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
@@ -7974,15 +7572,11 @@
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz",
|
||||
"integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-jalali": {
|
||||
"version": "4.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
@@ -12362,6 +11956,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -14272,21 +13867,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
@@ -14515,35 +14095,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "9.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz",
|
||||
"integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==",
|
||||
"dependencies": {
|
||||
"@date-fns/tz": "^1.4.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-jalali": "^4.1.0-0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/gpbl"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker/node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@noahspan/flying",
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "2.1.5",
|
||||
"scripts": {
|
||||
"start": "node api/dist/main",
|
||||
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
||||
|
||||
Reference in New Issue
Block a user