migrating to sqlite
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"jwks-rsa": "^3.2.0",
|
"jwks-rsa": "^3.2.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"node-gyp": "^11.4.1",
|
"node-gyp": "^11.4.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ import { join } from 'path';
|
|||||||
load: [configuration]
|
load: [configuration]
|
||||||
}),
|
}),
|
||||||
// HealthModule,
|
// HealthModule,
|
||||||
// LogModule,
|
LogModule,
|
||||||
PilotModule,
|
PilotModule,
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: join(__dirname, '../..', 'client', 'dist')
|
rootPath: join(__dirname, '../..', 'client', 'dist')
|
||||||
}),
|
}),
|
||||||
// TrackModule,
|
TrackModule,
|
||||||
TypeOrmModule.forRoot(dataSourceOptions),
|
TypeOrmModule.forRoot(dataSourceOptions),
|
||||||
MsGraphModule.registerAsync({
|
MsGraphModule.registerAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
|||||||
5
api/src/interfaces/customJwtPayload.interface.ts
Normal file
5
api/src/interfaces/customJwtPayload.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { JwtPayload } from "jwt-decode";
|
||||||
|
|
||||||
|
export interface CustomJwtPayload extends JwtPayload {
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
|
||||||
import { Observable, map } from 'rxjs';
|
|
||||||
import { LogEntity } from '../log.entity';
|
|
||||||
|
|
||||||
export class LogInterceptor implements NestInterceptor {
|
|
||||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
|
||||||
const req = context.switchToHttp().getRequest();
|
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return handler.handle().pipe(
|
|
||||||
map((data: LogEntity[]) => {
|
|
||||||
|
|
||||||
if (data.length) {
|
|
||||||
const logs = 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,
|
|
||||||
notes: log.notes
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return logs;
|
|
||||||
} else {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.handle().pipe(map((data) => data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,13 @@ import { LogEntity } from './log.entity';
|
|||||||
import { LogService } from './log.service';
|
import { LogService } from './log.service';
|
||||||
import { CustomError } from '../error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
import { AuthGuard } from '@noahspan/noahspan-modules';
|
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||||
import { LogInterceptor } from './interceptors/log.interceptor';
|
import { LogInterceptor } from './log.interceptor';
|
||||||
import { FileService } from '../file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||||
|
|
||||||
@Controller('logs')
|
@Controller('logs')
|
||||||
|
@UseInterceptors(new LogInterceptor())
|
||||||
|
// @UseGuards(AuthGuard)
|
||||||
export class LogController {
|
export class LogController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fileService: FileService,
|
private readonly fileService: FileService,
|
||||||
@@ -28,11 +30,11 @@ export class LogController {
|
|||||||
|
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@UseInterceptors(new LogInterceptor())
|
|
||||||
async find(
|
async find(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
): Promise<LogEntity> {
|
): Promise<LogEntity> {
|
||||||
try {
|
try {
|
||||||
|
console.log(id)
|
||||||
return await this.logService.find(id);
|
return await this.logService.find(id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
@@ -42,7 +44,6 @@ export class LogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseInterceptors(new LogInterceptor())
|
|
||||||
async findAll(): Promise<LogEntity[]> {
|
async findAll(): Promise<LogEntity[]> {
|
||||||
try {
|
try {
|
||||||
return await this.logService.findAll();
|
return await this.logService.findAll();
|
||||||
@@ -53,7 +54,7 @@ export class LogController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
||||||
try {
|
try {
|
||||||
@@ -65,22 +66,22 @@ export class LogController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
async update(
|
async update(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() logDto: LogDto
|
@Body() logDto: LogDto
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
try {
|
try {
|
||||||
|
console.log(id)
|
||||||
|
console.log(logDto)
|
||||||
return await this.logService.update(id, logDto);
|
return await this.logService.update(id, logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
|
console.log(error)
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async delete(
|
async delete(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ export class LogDto {
|
|||||||
solo?: number;
|
solo?: number;
|
||||||
pilotInCommand?: number;
|
pilotInCommand?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
pilot?: PilotEntity;
|
tracks?: []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export class LogEntity {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
pilotId: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|
||||||
|
|||||||
39
api/src/log/log.interceptor.ts
Normal file
39
api/src/log/log.interceptor.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { Observable, map } from 'rxjs';
|
||||||
|
import { LogEntity } from './log.entity';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
|
||||||
|
|
||||||
|
export class LogInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||||
|
|
||||||
|
return handler.handle().pipe(
|
||||||
|
map((data: LogEntity[]) => {
|
||||||
|
if (data.length > 0 && jwtPayload.roles.includes('Flying.Read')) {
|
||||||
|
const logs = 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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return logs;
|
||||||
|
} else {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ export class LogService {
|
|||||||
async find(id: string): Promise<LogEntity> {
|
async find(id: string): Promise<LogEntity> {
|
||||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
const logEntity: LogEntity = await this.logRepository.findOne({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
relations: ['pilot', 'tracks']
|
// relations: ['pilot', 'tracks']
|
||||||
});
|
});
|
||||||
|
|
||||||
return logEntity;
|
return logEntity;
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
|
||||||
import { Observable, map } from 'rxjs';
|
|
||||||
|
|
||||||
export class PilotInterceptor implements NestInterceptor {
|
|
||||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
|
||||||
const req = context.switchToHttp().getRequest();
|
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return handler.handle().pipe(
|
|
||||||
map((data) => {
|
|
||||||
if (data.length) {
|
|
||||||
const pilots = data.map((pilot) => {
|
|
||||||
return {
|
|
||||||
partitionKey: pilot.partitionKey,
|
|
||||||
rowKey: pilot.rowKey,
|
|
||||||
id: pilot.id,
|
|
||||||
name: pilot.name,
|
|
||||||
certificates: pilot.certificates,
|
|
||||||
endorsements: pilot.endorsements
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return pilots;
|
|
||||||
} else {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.handle().pipe(map((data) => data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,11 +14,12 @@ import { PilotDto } from './pilot.dto';
|
|||||||
import { PilotEntity } from './pilot.entity';
|
import { PilotEntity } from './pilot.entity';
|
||||||
import { PilotService } from './pilot.service';
|
import { PilotService } from './pilot.service';
|
||||||
import { CustomError } from '../error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
import { PilotInterceptor } from './interceptors/pilot.interceptor';
|
import { PilotInterceptor } from './pilot.interceptor';
|
||||||
import { AuthGuard } from '@noahspan/noahspan-modules';
|
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||||
|
|
||||||
@Controller('pilots')
|
@Controller('pilots')
|
||||||
// @UseInterceptors(new PilotInterceptor())
|
@UseInterceptors(new PilotInterceptor())
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
export class PilotController {
|
export class PilotController {
|
||||||
constructor(private readonly pilotService: PilotService) {}
|
constructor(private readonly pilotService: PilotService) {}
|
||||||
|
|
||||||
@@ -34,7 +35,6 @@ export class PilotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
async findAll() {
|
async findAll() {
|
||||||
try {
|
try {
|
||||||
return await this.pilotService.findAll();
|
return await this.pilotService.findAll();
|
||||||
@@ -45,7 +45,6 @@ export class PilotController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() pilotDto: PilotDto) {
|
async create(@Body() pilotDto: PilotDto) {
|
||||||
try {
|
try {
|
||||||
@@ -57,7 +56,6 @@ export class PilotController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
async update(
|
async update(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@@ -72,7 +70,6 @@ export class PilotController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async delete(
|
async delete(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
|
|||||||
33
api/src/pilot/pilot.interceptor.ts
Normal file
33
api/src/pilot/pilot.interceptor.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import { Observable, map } from 'rxjs';
|
||||||
|
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
|
||||||
|
import { PilotEntity } from './pilot.entity';
|
||||||
|
|
||||||
|
export class PilotInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||||
|
|
||||||
|
return handler.handle().pipe(
|
||||||
|
map((data: PilotEntity[]) => {
|
||||||
|
if (data.length && jwtPayload.roles.includes('Flying.Read')) {
|
||||||
|
const pilots = data.map((pilot) => {
|
||||||
|
return {
|
||||||
|
id: pilot.id,
|
||||||
|
name: pilot.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return pilots;
|
||||||
|
} else if (data.length && jwtPayload.roles.includes('Flying.Write')) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
return UnauthorizedException;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,9 +39,11 @@ export class TrackController {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
@Get(':logId')
|
@Get(':logId')
|
||||||
async findAll(@Param('logId') logId: string): Promise<TrackEntity[]> {
|
async findAll(@Param('logId') logId: string): Promise<TrackEntity[]> {
|
||||||
try {
|
try {
|
||||||
|
console.log('logId: ' + logId)
|
||||||
return await this.trackService.findAll(logId);
|
return await this.trackService.findAll(logId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export class TrackService {
|
|||||||
|
|
||||||
if (logEntity) {
|
if (logEntity) {
|
||||||
const url = await this.fileService.uploadFile(file, this.containerName, logId);
|
const url = await this.fileService.uploadFile(file, this.containerName, logId);
|
||||||
|
console.log(url)
|
||||||
const track = this.trackRepository.create({
|
const track = this.trackRepository.create({
|
||||||
log: logEntity,
|
log: logEntity,
|
||||||
order: order,
|
order: order,
|
||||||
|
|||||||
@@ -11,9 +11,14 @@
|
|||||||
"serve": "serve -s dist"
|
"serve": "serve -s dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noahspan/noahspan-components": "^2.0.0-alpha-11",
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||||
|
"@noahspan/noahspan-components": "^2.0.0-alpha-14",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"daisyui": "^5.1.10",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"oidc-spa": "^7.2.4",
|
"oidc-spa": "^7.2.4",
|
||||||
|
|||||||
@@ -3,28 +3,22 @@ import Flights from './components/flights/Flights';
|
|||||||
import Logbook from './components/logbook/Logbook';
|
import Logbook from './components/logbook/Logbook';
|
||||||
import Pilots from './components/pilots/Pilots';
|
import Pilots from './components/pilots/Pilots';
|
||||||
import SiteNav from './components/siteNav/SiteNav';
|
import SiteNav from './components/siteNav/SiteNav';
|
||||||
import { useAuth } from 'react-oidc-context';
|
import { HeroUIProvider } from '@heroui/react';
|
||||||
|
import { useHref, useNavigate } from 'react-router-dom';
|
||||||
interface ProtectedRouteProps {
|
import './styles.css';
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const auth = useAuth();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|
||||||
return auth.isAuthenticated ? children : <Navigate to='/' />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
||||||
<SiteNav />
|
<SiteNav />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Flights />} />
|
<Route path='/' element={<Flights />} />
|
||||||
<Route path="/logbook" element={<Logbook />} />
|
<Route path="/logbook" element={<Logbook />} />
|
||||||
<Route path="/pilots" element={<Pilots />} />
|
<Route path="/pilots" element={<Pilots />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</HeroUIProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { createReactOidc } from "oidc-spa/react";
|
import { createReactOidc } from "oidc-spa/react";
|
||||||
|
|
||||||
export const { OidcProvider, useOidc, getOidc, withLoginEnforced, enforceLogin } = createReactOidc(async () => ({
|
export const { OidcProvider, useOidc, getOidc } = createReactOidc(async () => ({
|
||||||
issuerUri: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}/v2.0`,
|
issuerUri: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}/v2.0`,
|
||||||
clientId: import.meta.env.VITE_CLIENT_APP_ID,
|
clientId: import.meta.env.VITE_CLIENT_APP_ID,
|
||||||
homeUrl: import.meta.env.BASE_URL,
|
homeUrl: import.meta.env.BASE_URL,
|
||||||
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`]
|
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`],
|
||||||
|
autoLogin: true,
|
||||||
|
postLoginRedirectUrl: '/',
|
||||||
|
noIframe: true
|
||||||
}));
|
}));
|
||||||
@@ -3,27 +3,42 @@ import { IActionMenuProps } from './IActionMenuProps';
|
|||||||
import {
|
import {
|
||||||
IconButton,
|
IconButton,
|
||||||
Icon,
|
Icon,
|
||||||
IconName
|
IconName,
|
||||||
|
Dropdown
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { useAuth } from 'react-oidc-context';
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
|
||||||
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
||||||
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
// const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
||||||
null
|
// null
|
||||||
);
|
// );
|
||||||
const auth = useAuth();
|
// const auth = useAuth();
|
||||||
|
|
||||||
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
// const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorElAction(event.currentTarget);
|
// setAnchorElAction(event.currentTarget);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onCloseActionMenu = () => {
|
// const onCloseActionMenu = () => {
|
||||||
setAnchorElAction(null);
|
// setAnchorElAction(null);
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
'Item 1',
|
||||||
|
'Item 2'
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<></>
|
<>
|
||||||
|
<Dropdown
|
||||||
|
onOptionSelected={() => console.log('clicked!')}
|
||||||
|
options={options}
|
||||||
|
>
|
||||||
|
<IconButton>
|
||||||
|
<Icon className='text-2xl' iconName={IconName.ELLIPSIS_VERTICAL} />
|
||||||
|
</IconButton>
|
||||||
|
</Dropdown>
|
||||||
|
</>
|
||||||
// <div>
|
// <div>
|
||||||
// <IconButton onClick={onOpenActionMenu}>
|
// <IconButton onClick={onOpenActionMenu}>
|
||||||
// <Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
// <Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
||||||
|
|||||||
@@ -1,37 +1,56 @@
|
|||||||
import { Card, Skeleton } from "@noahspan/noahspan-components";
|
import { Icon, IconName, Skeleton } from "@noahspan/noahspan-components";
|
||||||
|
import { Card } from '@heroui/react';
|
||||||
import LogbookCard from "../logbookCard/LogbookCard";
|
import LogbookCard from "../logbookCard/LogbookCard";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useReducer } from "react";
|
||||||
import { useLogs } from "../../hooks/logs/UseLogs";
|
import { useLogs } from "../../hooks/logs/UseLogs";
|
||||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
|
import { initialState, reducer } from "./reducer";
|
||||||
|
import { Alert } from '@heroui/react'
|
||||||
|
|
||||||
const Flights = () => {
|
const Flights = () => {
|
||||||
const [flights, setFlights] = useState<ILogbookEntry[]>([]);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const { logs, isLoading } = useLogs();
|
const { logs, logsLoading } = useLogs();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(logs)
|
const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => {
|
||||||
const flights: ILogbookEntry[] | undefined = logs?.filter((log: ILogbookEntry) => {
|
|
||||||
if (log.tracks && log.tracks.length > 0) {
|
if (log.tracks && log.tracks.length > 0) {
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (flights && flights.length > 0) {
|
if (flights && flights.length > 0) {
|
||||||
setFlights(flights)
|
dispatch({ type: 'SET_FLIGHTS', payload: flights})
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'There are no flights' }})
|
||||||
}
|
}
|
||||||
}, [logs])
|
}, [logs])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(logsLoading)
|
||||||
|
}, [logsLoading])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-screen-lg mx-auto'>
|
<div className='max-w-screen-lg mx-auto'>
|
||||||
<div className='prose mt-5 mb-5'>
|
<div className='prose mt-5 mb-5'>
|
||||||
<h1>Flights</h1>
|
<h1>Flights</h1>
|
||||||
</div>
|
</div>
|
||||||
{!isLoading &&
|
{!logsLoading && state.alert && (
|
||||||
<div>
|
<div>
|
||||||
<LogbookCard logs={flights} mode='flights' />
|
<Alert
|
||||||
|
onClose={() =>
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
|
}
|
||||||
|
color={state.alert.severity}
|
||||||
|
title={state.alert.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!logsLoading &&
|
||||||
|
<div>
|
||||||
|
<LogbookCard logs={state.flights} mode='flights' />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{!isLoading && [...Array(6)].map((_element, index) => {
|
{logsLoading && [...Array(6)].map((_element, index) => {
|
||||||
return (
|
return (
|
||||||
<div className='mb-5'>
|
<div className='mb-5'>
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
8
client/src/components/flights/FlightsState.interface.ts
Normal file
8
client/src/components/flights/FlightsState.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Alert } from "../../interfaces/Alert.interface";
|
||||||
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
|
|
||||||
|
export interface FlightsState {
|
||||||
|
alert: Alert | undefined;
|
||||||
|
flights: LogbookEntry[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
44
client/src/components/flights/reducer.tsx
Normal file
44
client/src/components/flights/reducer.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Alert } from "../../interfaces/Alert.interface";
|
||||||
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
|
import { FlightsState } from "./FlightsState.interface";
|
||||||
|
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
|
| { type: 'SET_FLIGHTS'; payload: LogbookEntry[] }
|
||||||
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
|
|
||||||
|
export const initialState: FlightsState = {
|
||||||
|
alert: undefined,
|
||||||
|
flights: [],
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (
|
||||||
|
state: FlightsState,
|
||||||
|
action: Action
|
||||||
|
): FlightsState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_ALERT': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alert: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_FLIGHTS': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
flights: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_IS_LOADING': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { FormMode } from '../../enums/formMode';
|
|
||||||
|
|
||||||
export interface ILogFormProps {
|
|
||||||
logId?: string;
|
|
||||||
isDrawerOpen: boolean;
|
|
||||||
mode: FormMode;
|
|
||||||
onOpenClose: (mode: FormMode) => void;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Alert } from "../../interfaces/Alert.interface";
|
|
||||||
|
|
||||||
export interface ILogFormState {
|
|
||||||
alert: Alert | undefined;
|
|
||||||
experienceCollapseOpen: boolean;
|
|
||||||
instrumentCollapseOpen: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
landingsCollapseOpen: boolean;
|
|
||||||
pilotOptions: { label: string; value: string }[];
|
|
||||||
selectedPilotName: string;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
8
client/src/components/logForm/LogFormProps.interface.ts
Normal file
8
client/src/components/logForm/LogFormProps.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { FormMode } from '../../enums/formMode';
|
||||||
|
|
||||||
|
export interface LogFormProps {
|
||||||
|
logId?: string;
|
||||||
|
// isDrawerOpen: boolean;
|
||||||
|
mode: FormMode;
|
||||||
|
// onOpenClose: (mode: FormMode) => void;
|
||||||
|
}
|
||||||
13
client/src/components/logForm/LogFormState.interface.ts
Normal file
13
client/src/components/logForm/LogFormState.interface.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Selection } from "@heroui/react";
|
||||||
|
import { Alert } from "../../interfaces/Alert.interface";
|
||||||
|
|
||||||
|
export interface LogFormState {
|
||||||
|
alert: Alert | undefined;
|
||||||
|
experienceSelectedKeys: Selection;
|
||||||
|
instrumentSelectedKeys: Selection;
|
||||||
|
isDisabled: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
landingsSelectedKeys: Selection;
|
||||||
|
pilotOptions: { key: string; label: string; }[];
|
||||||
|
selectedPilotName: string;
|
||||||
|
}
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { ILogFormState } from './ILogFormState';
|
import { LogFormState } from './LogFormState.interface';
|
||||||
|
import { Selection } from '@heroui/react';
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
| { type: 'SET_EXPERIENCE_COLLAPSE_OPEN'; payload: boolean }
|
| { type: 'SET_EXPERIENCE_SELECTED_KEYS'; payload: Selection }
|
||||||
| { type: 'SET_INSTRUMENT_COLLAPSE_OPEN'; payload: boolean }
|
| { type: 'SET_INSTRUMENT_SELECTED_KEYS'; payload: Selection }
|
||||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
| { type: 'SET_LANDINGS_COLLAPSE_OPEN'; payload: boolean }
|
| { type: 'SET_LANDINGS_SELECTED_KEYS'; payload: Selection }
|
||||||
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
| { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] }
|
||||||
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
||||||
|
|
||||||
export const initialState: ILogFormState = {
|
export const initialState: LogFormState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
experienceCollapseOpen: true,
|
experienceSelectedKeys: new Set([]),
|
||||||
instrumentCollapseOpen: false,
|
instrumentSelectedKeys: new Set([]),
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
landingsCollapseOpen: true,
|
landingsSelectedKeys: new Set(['1']),
|
||||||
pilotOptions: [],
|
pilotOptions: [],
|
||||||
selectedPilotName: ''
|
selectedPilotName: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
state: ILogFormState,
|
state: LogFormState,
|
||||||
action: Action
|
action: Action
|
||||||
): ILogFormState => {
|
): LogFormState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_ALERT': {
|
case 'SET_ALERT': {
|
||||||
return {
|
return {
|
||||||
@@ -33,10 +34,10 @@ export const reducer = (
|
|||||||
alert: action.payload
|
alert: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_EXPERIENCE_COLLAPSE_OPEN': {
|
case 'SET_EXPERIENCE_SELECTED_KEYS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
experienceCollapseOpen: action.payload
|
experienceSelectedKeys: action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'SET_IS_DISABLED': {
|
case 'SET_IS_DISABLED': {
|
||||||
@@ -45,10 +46,10 @@ export const reducer = (
|
|||||||
isDisabled: action.payload
|
isDisabled: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_INSTRUMENT_COLLAPSE_OPEN': {
|
case 'SET_INSTRUMENT_SELECTED_KEYS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
instrumentCollapseOpen: action.payload
|
instrumentSelectedKeys: action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'SET_IS_LOADING': {
|
case 'SET_IS_LOADING': {
|
||||||
@@ -57,10 +58,10 @@ export const reducer = (
|
|||||||
isLoading: action.payload
|
isLoading: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_LANDINGS_COLLAPSE_OPEN': {
|
case 'SET_LANDINGS_SELECTED_KEYS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
landingsCollapseOpen: action.payload
|
landingsSelectedKeys: action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'SET_PILOT_OPTIONS': {
|
case 'SET_PILOT_OPTIONS': {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
|
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
|
||||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { useAuth } from 'react-oidc-context'
|
import { useAuth } from 'react-oidc-context'
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||||
@@ -10,10 +9,10 @@ import 'swiper/css/pagination';
|
|||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import './LogTrackMaps.css';
|
import './LogTrackMaps.css';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import httpClient from '../../httpClient/httpClient'
|
||||||
|
|
||||||
const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
||||||
const [kmls, setKmls] = useState<any[]>([])
|
const [kmls, setKmls] = useState<any[]>([])
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
import { useEffect, useReducer } from "react";
|
|
||||||
import { Button, Drawer, Icon, IconButton, IconName, Input, Loading } from "@noahspan/noahspan-components";
|
|
||||||
import { useHttpClient } from "../../hooks/httpClient/UseHttpClient";
|
|
||||||
import { AxiosInstance, AxiosResponse } from "axios";
|
|
||||||
import { LogTracksProps } from "./LogTracksProps.interface";
|
|
||||||
import { FormMode } from "../../enums/formMode";
|
|
||||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
|
||||||
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
|
||||||
import { initialState, reducer } from "./reducer";
|
|
||||||
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
|
||||||
import { useAuth } from "react-oidc-context";
|
|
||||||
|
|
||||||
const LogTracks = ({ isDrawerOpen, mode, onOpenClose, selectedLogId }: LogTracksProps) => {
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState)
|
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
|
||||||
const auth = useAuth();
|
|
||||||
|
|
||||||
const getConfig = async () => {
|
|
||||||
const config = auth.isAuthenticated
|
|
||||||
? { headers: { Authorization: auth.user?.access_token } }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
// const getLog = async (): Promise<ILogbookEntry> => {
|
|
||||||
// const logResponse: AxiosResponse = await httpClient.get(
|
|
||||||
// `api/tracks/${selectedLogId}`,
|
|
||||||
// await getConfig()
|
|
||||||
// );
|
|
||||||
// const logData: ILogbookEntry = logResponse.data;
|
|
||||||
|
|
||||||
// return logData
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>, order: number) => {
|
|
||||||
// try {
|
|
||||||
// dispatch({ type: 'SET_IS_LOADING', payload: true})
|
|
||||||
|
|
||||||
// const file = event.target.files![0]
|
|
||||||
// const formData = new FormData();
|
|
||||||
// const config = await getConfig();
|
|
||||||
// const formDataConfig = {
|
|
||||||
// headers: {
|
|
||||||
// ...config.headers,
|
|
||||||
// 'Content-Type': 'multipart/form-data'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// formData.append('file', file);
|
|
||||||
|
|
||||||
// const uploadResponse: AxiosResponse = await httpClient.post(`api/tracks/${selectedLogId}/${order}`, formData, formDataConfig);
|
|
||||||
// const uploadUrl = uploadResponse.data.url;
|
|
||||||
// const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : [];
|
|
||||||
|
|
||||||
// tracks.push(uploadUrl)
|
|
||||||
// log.tracks = JSON.stringify(tracks);
|
|
||||||
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
|
|
||||||
|
|
||||||
// const updatedLog = await getLog();
|
|
||||||
|
|
||||||
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(error)
|
|
||||||
// } finally {
|
|
||||||
// dispatch({ type: 'SET_IS_LOADING', payload: false });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
onOpenClose(FormMode.CANCEL)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteTrack = async (fileName: string, index: number) => {
|
|
||||||
dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { fileName: fileName, index: index }}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// const onConfirmDialogConfirm = async () => {
|
|
||||||
// try {
|
|
||||||
// const config = await getConfig();
|
|
||||||
|
|
||||||
// await httpClient.delete(`api/logs/log/${selectedRowKey}/track?fileName=${state.selectedTrack!.fileName}`, config);
|
|
||||||
|
|
||||||
// const log = await getLog();
|
|
||||||
// const tracks: string[] = JSON.parse(log.tracks!);
|
|
||||||
|
|
||||||
// tracks.splice(state.selectedTrack!.index, 1);
|
|
||||||
// log.tracks = JSON.stringify(tracks);
|
|
||||||
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
|
|
||||||
|
|
||||||
// const updatedLog = await getLog();
|
|
||||||
|
|
||||||
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
|
||||||
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const onConfirmDialogCancel = async () => {
|
|
||||||
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const updateTracks = async () => {
|
|
||||||
// const log = await getLog();
|
|
||||||
|
|
||||||
// dispatch({ type: 'SET_TRACKS', payload: log.tracks! });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// updateTracks();
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDrawerOpen) {
|
|
||||||
console.log(selectedLogId)
|
|
||||||
const getTracks = async () => {
|
|
||||||
const tracks = await httpClient.get(`api/tracks/${selectedLogId}`);
|
|
||||||
|
|
||||||
console.log(tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTracks();
|
|
||||||
}
|
|
||||||
}, [isDrawerOpen])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
open={isDrawerOpen}
|
|
||||||
position='right'
|
|
||||||
width='25%'
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h4>{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Tracks`}</h4>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton disabled={state.isLoading ? true : false} onClick={onCancel}>
|
|
||||||
<Icon iconName={IconName.XMARK} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
{mode === FormMode.EDIT &&
|
|
||||||
<>
|
|
||||||
{state.isLoading &&
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Loading size='xl' />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{!state.isLoading && state.tracks.length > 0 && state.tracks.map((track, index) => {
|
|
||||||
const trackSplit = track.url.split('/')
|
|
||||||
const filename = trackSplit[trackSplit.length - 1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Input disabled={true} value={filename} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton onClick={() => onDeleteTrack(filename, index)}><Icon iconName={IconName.TRASH} /></IconButton>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
disabled={state.isLoading ? true : false}
|
|
||||||
startContent={<Icon iconName={IconName.XMARK} />}
|
|
||||||
onClick={onCancel}
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
{mode.toString() !== FormMode.VIEW && (
|
|
||||||
<Button
|
|
||||||
disabled={state.isLoading ? true : false}
|
|
||||||
startContent={<Icon iconName={IconName.UPLOAD} />}
|
|
||||||
>
|
|
||||||
Upload Track
|
|
||||||
{/* <input hidden onChange={handleFileUpload} type='file' /> */}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{mode === FormMode.VIEW &&
|
|
||||||
<LogTrackMaps logId={selectedLogId!} tracks={state.tracks} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{/* {state.isConfirmDialogOpen && (
|
|
||||||
<ConfirmationDialog
|
|
||||||
contentText="Are you sure you want to delete this track?"
|
|
||||||
isLoading={state.isConfirmDialogLoading}
|
|
||||||
isOpen={state.isConfirmDialogOpen}
|
|
||||||
onCancel={onConfirmDialogCancel}
|
|
||||||
onConfirm={onConfirmDialogConfirm}
|
|
||||||
title="Confirm Delete"
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LogTracks;
|
|
||||||
@@ -1,65 +1,374 @@
|
|||||||
import { useEffect, useReducer } from 'react';
|
import { Key, useEffect, useReducer } from 'react';
|
||||||
import LogForm from '../logForm/LogForm';
|
import LogForm from '../logForm/LogForm';
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
ColumnDef,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
IconName,
|
|
||||||
Loading,
|
|
||||||
Table
|
|
||||||
} from '@noahspan/noahspan-components';
|
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { authColumns, unauthColumns } from './columns';
|
import { authColumns, unauthColumns } from './columns';
|
||||||
import ActionMenu from '../actionMenu/ActionMenu';
|
import ActionMenu from '../actionMenu/ActionMenu';
|
||||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||||
import { ILogbookEntry } from './ILogbookEntry';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
import LogbookCard from '../logbookCard/LogbookCard';
|
import LogbookCard from '../logbookCard/LogbookCard';
|
||||||
import LogTracks from '../logTracks/LogTracks';
|
|
||||||
import { useOidc } from '../../auth/oidcConfig';
|
import { useOidc } from '../../auth/oidcConfig';
|
||||||
|
import httpClient from '../../httpClient/httpClient';
|
||||||
|
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||||
|
import { UserRole } from '../../enums/userRole';
|
||||||
|
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||||
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
|
import { Table, TableHeader, TableBody, TableColumn, Dropdown, DropdownTrigger, Button, DropdownSection, DropdownMenu, DropdownItem, Alert, TableRow, TableCell } from '@heroui/react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table';
|
||||||
|
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
||||||
|
import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
|
||||||
|
|
||||||
const Logbook: React.FC<unknown> = () => {
|
const Logbook: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const logbookContext = useLogbookContext()
|
||||||
const { isUserLoggedIn } = useOidc()
|
const { isUserLoggedIn } = useOidc();
|
||||||
const actionsColumn: ColumnDef<ILogbookEntry> = {
|
const { userRole } = useUserRole();
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
|
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||||
|
const blah = info.table
|
||||||
|
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
||||||
|
let total: number = 0;
|
||||||
|
|
||||||
|
if (values.length > 0) {
|
||||||
|
total = values.reduce((accumulator, currentValue) => accumulator + currentValue, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
const pilotName: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'pilotName',
|
||||||
|
accessorKey: 'pilot',
|
||||||
|
header: 'Pilot',
|
||||||
|
footer: 'PAGE TOTALS',
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
|
const pilot: any = info.getValue();
|
||||||
|
|
||||||
|
return pilot.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const date: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'date',
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Date',
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
|
const date = new Date(info.getValue() as string);
|
||||||
|
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const aircraftMakeModel: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'aircraftMakeModel',
|
||||||
|
accessorKey: 'aircraftMakeModel',
|
||||||
|
header: 'Aircraft Make & Model'
|
||||||
|
}
|
||||||
|
const route: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'route',
|
||||||
|
header: 'Route of Flight',
|
||||||
|
meta: {
|
||||||
|
headerAlign: 'center'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'routeFrom',
|
||||||
|
accessorKey: 'routeFrom',
|
||||||
|
header: 'From'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'routeTo',
|
||||||
|
accessorKey: 'routeTo',
|
||||||
|
header: 'To'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const durationOfFlight: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'durationOfFlight',
|
||||||
|
accessorKey: 'durationOfFlight',
|
||||||
|
header: 'Duration Of Flight',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
||||||
|
}
|
||||||
|
const notes: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'notes',
|
||||||
|
accessorKey: 'notes',
|
||||||
|
header: 'Notes'
|
||||||
|
}
|
||||||
|
const actions: ColumnDef<LogbookEntry> = {
|
||||||
|
id: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'center',
|
align: 'center',
|
||||||
headerAlign: 'center'
|
headerAlign: 'center'
|
||||||
},
|
},
|
||||||
cell: (info: any) => (
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
<ActionMenu
|
return (
|
||||||
id={info.row.original.id}
|
<Dropdown>
|
||||||
onDelete={onDeleteLog}
|
<DropdownTrigger>
|
||||||
onOpenCloseForm={onOpenCloseLogForm}
|
<Button isIconOnly variant='light' size='lg'>
|
||||||
onOpenCloseTracks={onOpenCloseTracks}
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
/>
|
</Button>
|
||||||
)
|
</DropdownTrigger>
|
||||||
}
|
<DropdownMenu>
|
||||||
const tracksColumn: ColumnDef<ILogbookEntry> = {
|
<DropdownSection showDivider>
|
||||||
accessorKey: 'tracks',
|
<DropdownItem
|
||||||
header: 'Tracks',
|
key='edit'
|
||||||
cell: (info: any) => {
|
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}
|
||||||
if (info.row.original.tracks.length > 0) {
|
startContent={<FontAwesomeIcon icon={faPen} />}
|
||||||
return (
|
>
|
||||||
<IconButton onClick={() => onOpenCloseTracks(FormMode.VIEW, info.row.original.rowKey)}><Icon iconName={IconName.MAP_LOCATION_DOT} /></IconButton>
|
Edit
|
||||||
)
|
</DropdownItem>
|
||||||
}
|
<DropdownItem
|
||||||
|
key='view'
|
||||||
|
onPress={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faEye} />}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key='tracks'
|
||||||
|
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faMapLocationDot} />}
|
||||||
|
>
|
||||||
|
Tracks
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
<DropdownSection>
|
||||||
|
<DropdownItem
|
||||||
|
key='Delete'
|
||||||
|
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
||||||
|
pilotName,
|
||||||
|
date,
|
||||||
|
aircraftMakeModel,
|
||||||
|
route,
|
||||||
|
durationOfFlight,
|
||||||
|
notes
|
||||||
|
]
|
||||||
|
|
||||||
|
const authColumns: ColumnDef<LogbookEntry>[] = [
|
||||||
|
pilotName,
|
||||||
|
date,
|
||||||
|
aircraftMakeModel,
|
||||||
|
{
|
||||||
|
id: 'aircraftIdentity',
|
||||||
|
accessorKey: 'aircraftIdentity',
|
||||||
|
header: 'Aircraft Identity',
|
||||||
|
},
|
||||||
|
route,
|
||||||
|
durationOfFlight,
|
||||||
|
{
|
||||||
|
id: 'singleEngineLand',
|
||||||
|
accessorKey: 'singleEngineLand',
|
||||||
|
header: 'Single Engine Land',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'landings',
|
||||||
|
header: 'Landings',
|
||||||
|
meta: {
|
||||||
|
headerAlign: 'center'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'landingsDay',
|
||||||
|
accessorKey: 'landingsDay',
|
||||||
|
header: 'Day',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'landingsNight',
|
||||||
|
accessorKey: 'landingsNight',
|
||||||
|
header: 'Night',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instrument',
|
||||||
|
header: 'Instrument',
|
||||||
|
meta: {
|
||||||
|
headerAlign: 'center'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'instrumentActual',
|
||||||
|
accessorKey: 'instrumentActual',
|
||||||
|
header: 'Actual',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instrumentSimulated',
|
||||||
|
accessorKey: 'instrumentSimulated',
|
||||||
|
header: 'Simulated',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instrumentApproaches',
|
||||||
|
accessorKey: 'instrumentApproaches',
|
||||||
|
header: 'Approaches',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instrumentHolds',
|
||||||
|
accessorKey: 'instrumentHolds',
|
||||||
|
header: 'Holds',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instrumentNavTrack',
|
||||||
|
accessorKey: 'instrumentNavTrack',
|
||||||
|
header: 'Nav/Track',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'experienceTraining',
|
||||||
|
header: 'Type of pilot experience or training',
|
||||||
|
meta: {
|
||||||
|
headerAlign: 'center'
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'groundTrainingReceived',
|
||||||
|
accessorKey: 'groundTrainingReceived',
|
||||||
|
header: 'Ground Training Received',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flightTrainingReceived',
|
||||||
|
accessorKey: 'flightTrainingReceived',
|
||||||
|
header: 'Flight Training Received',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crossCountry',
|
||||||
|
accessorKey: 'crossCountry',
|
||||||
|
header: 'Cross Country',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night',
|
||||||
|
accessorKey: 'night',
|
||||||
|
header: 'Night',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'solo',
|
||||||
|
accessorKey: 'solo',
|
||||||
|
header: 'Solo',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pilotInCommand',
|
||||||
|
accessorKey: 'pilotInCommand',
|
||||||
|
header: 'Pilot In Command',
|
||||||
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right'
|
||||||
|
},
|
||||||
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
notes,
|
||||||
|
actions
|
||||||
|
]
|
||||||
|
|
||||||
const getLogbookEntries = async () => {
|
const getLogbookEntries = async () => {
|
||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||||
|
|
||||||
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
||||||
const entries: ILogbookEntry[] = response.data;
|
const entries: LogbookEntry[] = response.data;
|
||||||
console.log(entries)
|
|
||||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
if (response.data.length > 0) {
|
||||||
@@ -69,42 +378,43 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
dispatch({ type: 'SET_ALERT', payload: undefined})
|
dispatch({ type: 'SET_ALERT', payload: undefined})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No logbook entries found.'}})
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No logbook entries found.'}})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpenCloseLogForm = (mode: FormMode, logId?: string) => {
|
const onOpenCloseDrawer= (mode: FormMode, logId?: string) => {
|
||||||
|
console.log(logId)
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case FormMode.ADD:
|
case FormMode.ADD:
|
||||||
case FormMode.EDIT:
|
case FormMode.EDIT:
|
||||||
case FormMode.VIEW:
|
case FormMode.VIEW:
|
||||||
dispatch({
|
logbookContext.dispatch({
|
||||||
type: 'SET_OPEN_CLOSE_LOG_FORM',
|
type: 'SET_OPEN_CLOSE_DRAWER',
|
||||||
payload: {
|
payload: {
|
||||||
formMode: mode,
|
formMode: mode,
|
||||||
selectedLogId: logId,
|
selectedLogId: logId!,
|
||||||
isFormOpen: true
|
isDrawerOpen: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case FormMode.CANCEL:
|
case FormMode.CANCEL:
|
||||||
dispatch({
|
logbookContext.dispatch({
|
||||||
type: 'SET_OPEN_CLOSE_LOG_FORM',
|
type: 'SET_OPEN_CLOSE_DRAWER',
|
||||||
payload: {
|
payload: {
|
||||||
formMode: mode,
|
formMode: mode,
|
||||||
selectedLogId: undefined,
|
selectedLogId: undefined,
|
||||||
isFormOpen: false
|
isDrawerOpen: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,34 +422,6 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpenCloseTracks = (mode: FormMode, rowKey?: string) => {
|
|
||||||
switch(mode) {
|
|
||||||
case FormMode.EDIT:
|
|
||||||
case FormMode.VIEW:
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_OPEN_CLOSE_TRACKS',
|
|
||||||
payload: {
|
|
||||||
tracksMode: mode,
|
|
||||||
isTracksOpen: true,
|
|
||||||
selectedRowKey: rowKey
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
break;
|
|
||||||
case FormMode.CANCEL:
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_OPEN_CLOSE_TRACKS',
|
|
||||||
payload: {
|
|
||||||
tracksMode: mode,
|
|
||||||
isTracksOpen: false,
|
|
||||||
selectedRowKey: undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteLog = (logId: string) => {
|
const onDeleteLog = (logId: string) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_DELETE',
|
type: 'SET_DELETE',
|
||||||
@@ -151,7 +433,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
||||||
|
|
||||||
await httpClient.delete(`api/logs/${state.selectedLogId}`);
|
await httpClient.delete(`api/logs/${logbookContext.state.selectedLogId}`);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_DELETE',
|
type: 'SET_DELETE',
|
||||||
@@ -163,7 +445,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
||||||
@@ -177,36 +459,49 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
let newColumns: ColumnDef<ILogbookEntry>[];
|
// let newColumns: ColumnDef<LogbookEntry>[];
|
||||||
|
|
||||||
if (isUserLoggedIn) {
|
// if (userRole === UserRole.WRITE) {
|
||||||
newColumns = [...authColumns];
|
// newColumns = [...authColumns];
|
||||||
} else {
|
// } else {
|
||||||
newColumns = [...unauthColumns];
|
// newColumns = [...unauthColumns];
|
||||||
}
|
// }
|
||||||
|
|
||||||
const actionsColumnExists = newColumns.find((column) => column.id === 'actions');
|
// const actionsColumnExists = newColumns.find((column) => column.id === 'actions');
|
||||||
const tracksColumnExists = newColumns.find((column) => column.id === 'actions');
|
// const tracksColumnExists = newColumns.find((column) => column.id === 'actions');
|
||||||
|
|
||||||
if (!actionsColumnExists) {
|
// if (!actionsColumnExists) {
|
||||||
newColumns.push(actionsColumn);
|
// newColumns.push(actionsColumn);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!tracksColumnExists) {
|
// if (!tracksColumnExists) {
|
||||||
const notesColumnIndex = newColumns.findIndex((column) => column.id === 'notes')
|
// const notesColumnIndex = newColumns.findIndex((column) => column.id === 'notes')
|
||||||
|
|
||||||
newColumns.splice(notesColumnIndex, 0, tracksColumn)
|
// newColumns.splice(notesColumnIndex, 0, tracksColumn)
|
||||||
}
|
// }
|
||||||
|
|
||||||
dispatch({ type: 'SET_COLUMNS', payload: newColumns })
|
// dispatch({ type: 'SET_COLUMNS', payload: newColumns })
|
||||||
}, [isUserLoggedIn])
|
// }, [isUserLoggedIn])
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: state.entries,
|
||||||
|
columns: authColumns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel()
|
||||||
|
});
|
||||||
|
|
||||||
|
const textAlignment = {
|
||||||
|
center: 'text-center',
|
||||||
|
left: 'text-start',
|
||||||
|
right: 'text-end'
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isFormOpen) {
|
if (!logbookContext.state.isDrawerOpen) {
|
||||||
getLogbookEntries();
|
getLogbookEntries();
|
||||||
}
|
}
|
||||||
}, [state.isFormOpen, state.isTracksOpen]);
|
}, [logbookContext.state.isDrawerOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -215,12 +510,13 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
<h1>Logbook</h1>
|
<h1>Logbook</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
{isUserLoggedIn &&
|
{userRole === UserRole.WRITE &&
|
||||||
<Button
|
<Button
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => onOpenCloseLogForm(FormMode.ADD)}
|
onPress={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||||
startContent={<Icon iconName={IconName.PLUS} />}
|
startContent={<FontAwesomeIcon icon={faAdd} />}
|
||||||
data-testid="pilot-add-button"
|
data-testid="pilot-add-button"
|
||||||
|
data-theme="lofi"
|
||||||
>
|
>
|
||||||
Add Entry
|
Add Entry
|
||||||
</Button>
|
</Button>
|
||||||
@@ -232,23 +528,103 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
color={'default'}
|
||||||
>
|
title={state.alert.message}
|
||||||
{state.alert.message}
|
/>
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!state.isLoading && (
|
{!state.isLoading && (
|
||||||
<div>
|
<div className='col-span-12'>
|
||||||
{state.columns && state.columns.length > 0 && state.entries.length > 0 && (
|
{state.entries.length > 0 && screenSize !== ScreenSize.SM && (
|
||||||
<Table columns={state.columns} data={state.entries} />
|
<div className='p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto shadow-small rounded-large w-full'>
|
||||||
|
<table className='min-w-full h-auto table-auto w-full'>
|
||||||
|
<thead className='[&>tr]:first:rounded-lg'>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
className={`${header.column.columnDef.meta?.align ? textAlignment[header.column.columnDef.meta?.align] : ''} group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
key={header.id}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<div>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
{/* {header.column.getCanFilter() ? (
|
||||||
|
<div>
|
||||||
|
<Filter column={header.column} table={table} />
|
||||||
|
</div>
|
||||||
|
) : null} */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<>
|
||||||
|
{table.getRowModel().rows.map((row) => {
|
||||||
|
return (
|
||||||
|
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
className={`${cell.column.columnDef.meta?.align ? textAlignment[cell.column.columnDef.meta?.align] : ''} py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:z-1 [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
|
||||||
|
key={cell.id}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</tbody>
|
||||||
|
<thead className='[&>tr]:first:rounded-lg'>
|
||||||
|
{table.getFooterGroups().map((footerGroup, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return (
|
||||||
|
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={footerGroup.id}>
|
||||||
|
{footerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
className='group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
|
||||||
|
key={header.id}
|
||||||
|
align={header.column.columnDef.meta?.align}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.footer,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.entries.length > 0 &&
|
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseLogForm} />
|
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.isLoading && !state.alert && (
|
{/* {state.isLoading && !state.alert && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Loading size='xl' />
|
<Loading size='xl' />
|
||||||
@@ -257,14 +633,11 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
{state.isFormOpen && (
|
{logbookContext.state.isDrawerOpen && (
|
||||||
<LogForm
|
<LogbookDrawer
|
||||||
logId={state.selectedLogId}
|
onOpenClose={(mode) => onOpenCloseDrawer(mode)}
|
||||||
isDrawerOpen={state.isFormOpen}
|
|
||||||
mode={state.formMode}
|
|
||||||
onOpenClose={(mode) => onOpenCloseLogForm(mode)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{state.isConfirmDialogOpen && (
|
{state.isConfirmDialogOpen && (
|
||||||
@@ -277,14 +650,14 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
title="Confirm Delete"
|
title="Confirm Delete"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{state.isTracksOpen &&
|
{/* {state.isTracksOpen &&
|
||||||
<LogTracks
|
<LogTracks
|
||||||
isDrawerOpen={state.isTracksOpen}
|
isDrawerOpen={state.isTracksOpen}
|
||||||
mode={state.tracksMode}
|
mode={state.tracksMode}
|
||||||
onOpenClose={(mode) => onOpenCloseTracks(mode)}
|
onOpenClose={(mode) => onOpenCloseTracks(mode)}
|
||||||
selectedLogId={state.selectedLogId}
|
selectedLogId={logbookContext.state.selectedLogId}
|
||||||
/>
|
/>
|
||||||
}
|
} */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export interface ILogbookEntry {
|
import { Pilot } from "../pilots/Pilot.interface";
|
||||||
|
|
||||||
|
export interface LogbookEntry {
|
||||||
id: string;
|
id: string;
|
||||||
pilot: string;
|
pilot: Pilot;
|
||||||
date: string;
|
date: string;
|
||||||
aircraftMakeModel: string;
|
aircraftMakeModel: string;
|
||||||
aircraftIdentity: string;
|
aircraftIdentity: string;
|
||||||
@@ -22,6 +24,6 @@ export interface ILogbookEntry {
|
|||||||
night: number | null;
|
night: number | null;
|
||||||
solo: number | null;
|
solo: number | null;
|
||||||
pilotInCommand: number | null;
|
pilotInCommand: number | null;
|
||||||
tracks: {id: string; order: number; url: string}[];
|
tracks: [];
|
||||||
notes: string;
|
notes: string;
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
import { ColumnDef } from '@noahspan/noahspan-components';
|
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { ILogbookEntry } from './ILogbookEntry';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
|
|
||||||
export interface ILogbookState {
|
export interface LogbookState {
|
||||||
alert: Alert | undefined;
|
alert: Alert | undefined;
|
||||||
columns: ColumnDef<ILogbookEntry>[];
|
columns: ColumnDef<LogbookEntry>[];
|
||||||
entries: ILogbookEntry[];
|
entries: LogbookEntry[];
|
||||||
formMode: FormMode;
|
|
||||||
isConfirmDialogLoading: boolean;
|
isConfirmDialogLoading: boolean;
|
||||||
isConfirmDialogOpen: boolean;
|
isConfirmDialogOpen: boolean;
|
||||||
isFormOpen: boolean;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isTracksOpen: boolean;
|
|
||||||
selectedLogId: string | undefined;
|
|
||||||
tracksMode: FormMode;
|
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@ import {
|
|||||||
ColumnDef,
|
ColumnDef,
|
||||||
HeaderContext,
|
HeaderContext,
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { ILogbookEntry } from './ILogbookEntry';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
|
|
||||||
const columnTotal = (info: HeaderContext<ILogbookEntry, unknown>): number => {
|
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||||
const blah = info.table
|
const blah = info.table
|
||||||
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
||||||
let total: number = 0;
|
let total: number = 0;
|
||||||
@@ -17,34 +17,35 @@ const columnTotal = (info: HeaderContext<ILogbookEntry, unknown>): number => {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pilotName: ColumnDef<ILogbookEntry> = {
|
const pilotName: ColumnDef<LogbookEntry> = {
|
||||||
id: 'pilotName',
|
id: 'pilotName',
|
||||||
accessorKey: 'pilot',
|
accessorKey: 'pilot',
|
||||||
header: 'Pilot',
|
header: 'Pilot',
|
||||||
footer: 'PAGE TOTALS',
|
footer: 'PAGE TOTALS',
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) => {
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
const pilot: any = info.getValue();
|
const pilot: any = info.getValue();
|
||||||
|
|
||||||
return pilot.name
|
return pilot.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const date: ColumnDef<ILogbookEntry> = {
|
const date: ColumnDef<LogbookEntry> = {
|
||||||
id: 'date',
|
id: 'date',
|
||||||
accessorKey: 'date',
|
accessorKey: 'date',
|
||||||
header: 'Date',
|
header: 'Date',
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) => {
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
const date = new Date(info.getValue() as string);
|
const date = new Date(info.getValue() as string);
|
||||||
const formattedDate = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`
|
console.log(date)
|
||||||
|
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const aircraftMakeModel: ColumnDef<ILogbookEntry> = {
|
const aircraftMakeModel: ColumnDef<LogbookEntry> = {
|
||||||
id: 'aircraftMakeModel',
|
id: 'aircraftMakeModel',
|
||||||
accessorKey: 'aircraftMakeModel',
|
accessorKey: 'aircraftMakeModel',
|
||||||
header: 'Aircraft Make & Model'
|
header: 'Aircraft Make & Model'
|
||||||
}
|
}
|
||||||
const route: ColumnDef<ILogbookEntry> = {
|
const route: ColumnDef<LogbookEntry> = {
|
||||||
id: 'route',
|
id: 'route',
|
||||||
header: 'Route of Flight',
|
header: 'Route of Flight',
|
||||||
meta: {
|
meta: {
|
||||||
@@ -63,7 +64,7 @@ const route: ColumnDef<ILogbookEntry> = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const durationOfFlight: ColumnDef<ILogbookEntry> = {
|
const durationOfFlight: ColumnDef<LogbookEntry> = {
|
||||||
id: 'durationOfFlight',
|
id: 'durationOfFlight',
|
||||||
accessorKey: 'durationOfFlight',
|
accessorKey: 'durationOfFlight',
|
||||||
header: 'Duration Of Flight',
|
header: 'Duration Of Flight',
|
||||||
@@ -71,17 +72,17 @@ const durationOfFlight: ColumnDef<ILogbookEntry> = {
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
||||||
}
|
}
|
||||||
const notes: ColumnDef<ILogbookEntry> = {
|
const notes: ColumnDef<LogbookEntry> = {
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
accessorKey: 'notes',
|
accessorKey: 'notes',
|
||||||
header: 'Notes'
|
header: 'Notes'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unauthColumns: ColumnDef<ILogbookEntry>[] = [
|
export const unauthColumns: ColumnDef<LogbookEntry>[] = [
|
||||||
pilotName,
|
pilotName,
|
||||||
date,
|
date,
|
||||||
aircraftMakeModel,
|
aircraftMakeModel,
|
||||||
@@ -90,7 +91,7 @@ export const unauthColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
notes
|
notes
|
||||||
]
|
]
|
||||||
|
|
||||||
export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
export const authColumns: ColumnDef<LogbookEntry>[] = [
|
||||||
pilotName,
|
pilotName,
|
||||||
date,
|
date,
|
||||||
aircraftMakeModel,
|
aircraftMakeModel,
|
||||||
@@ -109,9 +110,9 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'landings',
|
id: 'landings',
|
||||||
@@ -124,7 +125,7 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
id: 'landingsDay',
|
id: 'landingsDay',
|
||||||
accessorKey: 'landingsDay',
|
accessorKey: 'landingsDay',
|
||||||
header: 'Day',
|
header: 'Day',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
@@ -134,7 +135,7 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
id: 'landingsNight',
|
id: 'landingsNight',
|
||||||
accessorKey: 'landingsNight',
|
accessorKey: 'landingsNight',
|
||||||
header: 'Night',
|
header: 'Night',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
@@ -153,31 +154,31 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
id: 'instrumentActual',
|
id: 'instrumentActual',
|
||||||
accessorKey: 'instrumentActual',
|
accessorKey: 'instrumentActual',
|
||||||
header: 'Actual',
|
header: 'Actual',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'instrumentSimulated',
|
id: 'instrumentSimulated',
|
||||||
accessorKey: 'instrumentSimulated',
|
accessorKey: 'instrumentSimulated',
|
||||||
header: 'Simulated',
|
header: 'Simulated',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'instrumentApproaches',
|
id: 'instrumentApproaches',
|
||||||
accessorKey: 'instrumentApproaches',
|
accessorKey: 'instrumentApproaches',
|
||||||
header: 'Approaches',
|
header: 'Approaches',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
@@ -187,7 +188,7 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
id: 'instrumentHolds',
|
id: 'instrumentHolds',
|
||||||
accessorKey: 'instrumentHolds',
|
accessorKey: 'instrumentHolds',
|
||||||
header: 'Holds',
|
header: 'Holds',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
@@ -197,7 +198,7 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
id: 'instrumentNavTrack',
|
id: 'instrumentNavTrack',
|
||||||
accessorKey: 'instrumentNavTrack',
|
accessorKey: 'instrumentNavTrack',
|
||||||
header: 'Nav/Track',
|
header: 'Nav/Track',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
@@ -216,72 +217,72 @@ export const authColumns: ColumnDef<ILogbookEntry>[] = [
|
|||||||
id: 'groundTrainingReceived',
|
id: 'groundTrainingReceived',
|
||||||
accessorKey: 'groundTrainingReceived',
|
accessorKey: 'groundTrainingReceived',
|
||||||
header: 'Ground Training Received',
|
header: 'Ground Training Received',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'flightTrainingReceived',
|
id: 'flightTrainingReceived',
|
||||||
accessorKey: 'flightTrainingReceived',
|
accessorKey: 'flightTrainingReceived',
|
||||||
header: 'Flight Training Received',
|
header: 'Flight Training Received',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'crossCountry',
|
id: 'crossCountry',
|
||||||
accessorKey: 'crossCountry',
|
accessorKey: 'crossCountry',
|
||||||
header: 'Cross Country',
|
header: 'Cross Country',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'night',
|
id: 'night',
|
||||||
accessorKey: 'night',
|
accessorKey: 'night',
|
||||||
header: 'Night',
|
header: 'Night',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'solo',
|
id: 'solo',
|
||||||
accessorKey: 'solo',
|
accessorKey: 'solo',
|
||||||
header: 'Solo',
|
header: 'Solo',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pilotInCommand',
|
id: 'pilotInCommand',
|
||||||
accessorKey: 'pilotInCommand',
|
accessorKey: 'pilotInCommand',
|
||||||
header: 'Pilot In Command',
|
header: 'Pilot In Command',
|
||||||
footer: (info: HeaderContext<ILogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
|
||||||
meta: {
|
meta: {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
headerAlign: 'right'
|
headerAlign: 'right'
|
||||||
},
|
},
|
||||||
cell: (info: CellContext<ILogbookEntry, unknown>) =>
|
cell: (info: CellContext<LogbookEntry, unknown>) =>
|
||||||
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ColumnDef } from '@noahspan/noahspan-components';
|
import { ColumnDef } from '@noahspan/noahspan-components';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { ILogbookEntry } from './ILogbookEntry';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
import { ILogbookState } from './ILogbookState';
|
import { LogbookState } from './LogbookState.interface';
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_COLUMNS'; payload: ColumnDef<ILogbookEntry>[] }
|
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
||||||
| {
|
| {
|
||||||
type: 'SET_DELETE';
|
type: 'SET_DELETE';
|
||||||
payload: {
|
payload: {
|
||||||
@@ -13,39 +13,25 @@ type Action =
|
|||||||
selectedLogId: string | undefined;
|
selectedLogId: string | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: 'SET_ENTRIES'; payload: ILogbookEntry[] }
|
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
| { type: 'SET_FORM_MODE'; payload: FormMode }
|
|
||||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
| { type: 'SET_IS_LOADING'; payload: boolean };
|
||||||
| {
|
|
||||||
type: 'SET_OPEN_CLOSE_LOG_FORM';
|
|
||||||
payload: {
|
|
||||||
formMode: FormMode;
|
|
||||||
selectedLogId: string | undefined;
|
|
||||||
isFormOpen: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| { type: 'SET_OPEN_CLOSE_TRACKS'; payload: { tracksMode: FormMode, selectedRowKey: string | undefined, isTracksOpen: boolean; }};
|
|
||||||
|
|
||||||
export const initialState: ILogbookState = {
|
|
||||||
|
export const initialState: LogbookState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
columns: [],
|
columns: [],
|
||||||
entries: [],
|
entries: [],
|
||||||
formMode: FormMode.CANCEL,
|
|
||||||
isConfirmDialogLoading: false,
|
isConfirmDialogLoading: false,
|
||||||
isConfirmDialogOpen: false,
|
isConfirmDialogOpen: false,
|
||||||
isFormOpen: false,
|
isLoading: false
|
||||||
isLoading: false,
|
|
||||||
isTracksOpen: false,
|
|
||||||
selectedLogId: undefined,
|
|
||||||
tracksMode: FormMode.CANCEL
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
state: ILogbookState,
|
state: LogbookState,
|
||||||
action: Action
|
action: Action
|
||||||
): ILogbookState => {
|
): LogbookState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_COLUMNS': {
|
case 'SET_COLUMNS': {
|
||||||
return {
|
return {
|
||||||
@@ -57,7 +43,6 @@ export const reducer = (
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isConfirmDialogOpen: action.payload.isConfirmationDialogOpen,
|
isConfirmDialogOpen: action.payload.isConfirmationDialogOpen,
|
||||||
selectedLogId: action.payload.selectedLogId
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_ENTRIES': {
|
case 'SET_ENTRIES': {
|
||||||
@@ -72,12 +57,6 @@ export const reducer = (
|
|||||||
alert: action.payload
|
alert: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_FORM_MODE': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
formMode: action.payload
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SET_IS_CONFIRMATION_DIALOG_LOADING': {
|
case 'SET_IS_CONFIRMATION_DIALOG_LOADING': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -90,22 +69,6 @@ export const reducer = (
|
|||||||
isLoading: action.payload
|
isLoading: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_OPEN_CLOSE_LOG_FORM': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
formMode: action.payload.formMode,
|
|
||||||
isFormOpen: action.payload.isFormOpen,
|
|
||||||
selectedLogId: action.payload.selectedLogId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SET_OPEN_CLOSE_TRACKS': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
tracksMode: action.payload.tracksMode,
|
|
||||||
isTracksOpen: action.payload.isTracksOpen,
|
|
||||||
selectedLogId: action.payload.selectedRowKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FormMode } from "../../enums/formMode";
|
import { FormMode } from "../../enums/formMode";
|
||||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
|
|
||||||
export interface LogbookCardProps {
|
export interface LogbookCardProps {
|
||||||
logs: ILogbookEntry[];
|
logs: LogbookEntry[];
|
||||||
mode: 'flights' | 'logbook';
|
mode: 'flights' | 'logbook';
|
||||||
onDelete?: (entryId: string) => void;
|
onDelete?: (entryId: string) => void;
|
||||||
onOpenCloseForm?: (formMode: FormMode, id: string) => void;
|
onOpenCloseForm?: (formMode: FormMode, id: string) => void;
|
||||||
|
|||||||
154
client/src/components/logbookDrawer/LogbookDrawer.tsx
Normal file
154
client/src/components/logbookDrawer/LogbookDrawer.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { Alert, Button, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Tab, Tabs } from "@heroui/react";
|
||||||
|
import { LogbookDrawerProps } from "./LogbookDrawerProps.interface";
|
||||||
|
import LogForm from "../logForm/LogForm";
|
||||||
|
import TracksForm from "../tracksForm/TracksForm";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faClock, faMapLocationDot, faSave, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { FormMode } from "../../enums/formMode";
|
||||||
|
import httpClient from "../../httpClient/httpClient";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||||
|
|
||||||
|
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||||
|
const defaultValues = {
|
||||||
|
pilotId: '',
|
||||||
|
date: null,
|
||||||
|
aircraftMakeModel: '',
|
||||||
|
aircraftIdentity: '',
|
||||||
|
routeFrom: '',
|
||||||
|
routeTo: '',
|
||||||
|
durationOfFlight: null,
|
||||||
|
singleEngineLand: null,
|
||||||
|
simulatorAtd: null,
|
||||||
|
landingsDay: null,
|
||||||
|
landingsNight: null,
|
||||||
|
groundTrainingReceived: null,
|
||||||
|
flightTrainingReceived: null,
|
||||||
|
crossCountry: null,
|
||||||
|
night: null,
|
||||||
|
solo: null,
|
||||||
|
pilotInCommand: null,
|
||||||
|
instrumentActual: null,
|
||||||
|
instrumentSimulated: null,
|
||||||
|
instrumentApproaches: null,
|
||||||
|
instrumentHolds: null,
|
||||||
|
instrumentNavTrack: null,
|
||||||
|
notes: '',
|
||||||
|
tracks: []
|
||||||
|
};
|
||||||
|
const methods = useForm();
|
||||||
|
const logbookContext = useLogbookContext()
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
methods.reset(defaultValues);
|
||||||
|
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
||||||
|
onOpenClose(FormMode.CANCEL);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: unknown) => {
|
||||||
|
console.log(data)
|
||||||
|
try {
|
||||||
|
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true });
|
||||||
|
|
||||||
|
if (!logbookContext.state.selectedLogId) {
|
||||||
|
await httpClient.post(`api/logs`, data);
|
||||||
|
} else {
|
||||||
|
await httpClient.put(`api/logs/${logbookContext.state.selectedLogId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
methods.reset(defaultValues);
|
||||||
|
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
|
||||||
|
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }});
|
||||||
|
} finally {
|
||||||
|
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
isOpen={logbookContext.state.isDrawerOpen}
|
||||||
|
>
|
||||||
|
<DrawerContent>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||||
|
<DrawerHeader>
|
||||||
|
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
|
||||||
|
</DrawerHeader>
|
||||||
|
<DrawerBody>
|
||||||
|
{logbookContext.state.formAlert && (
|
||||||
|
<div className='col-span-12'>
|
||||||
|
<Alert
|
||||||
|
onClose={() =>
|
||||||
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
|
||||||
|
}
|
||||||
|
color={logbookContext.state.formAlert.severity}
|
||||||
|
title={logbookContext.state.formAlert.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Tabs color='default' fullWidth={true} variant='solid'>
|
||||||
|
<Tab
|
||||||
|
key='time'
|
||||||
|
title={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FontAwesomeIcon icon={faClock} />
|
||||||
|
<span>Time</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LogForm />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
key='tracks'
|
||||||
|
title={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FontAwesomeIcon icon={faMapLocationDot} />
|
||||||
|
<span>Tracks</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TracksForm />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</DrawerBody>
|
||||||
|
<DrawerFooter>
|
||||||
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
|
<div className='col-span-12 justify-self-end self-center'>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
|
||||||
|
? logbookContext.state.isFormDisabled
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
startContent={<FontAwesomeIcon icon={faXmark} />}
|
||||||
|
onPress={onCancel}
|
||||||
|
>
|
||||||
|
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||||
|
</Button>
|
||||||
|
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
|
||||||
|
<Button
|
||||||
|
className='ml-[10px]'
|
||||||
|
color='primary'
|
||||||
|
disabled={logbookContext.state.isFormDisabled}
|
||||||
|
startContent={<FontAwesomeIcon icon={faSave} />}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerFooter>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogbookDrawer
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { FormMode } from "../../enums/formMode";
|
||||||
|
|
||||||
|
export interface LogbookDrawerProps {
|
||||||
|
onOpenClose: (mode: FormMode) => void;
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
Button,
|
// Button,
|
||||||
Drawer,
|
// Drawer,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconName,
|
IconName,
|
||||||
Input,
|
// Input,
|
||||||
PeoplePicker,
|
PeoplePicker,
|
||||||
StateSelect
|
StateSelect
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { IPilotFormProps } from './IPilotFormProps';
|
import { IPilotFormProps } from './IPilotFormProps';
|
||||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Person } from '@microsoft/microsoft-graph-types';
|
import { Person } from '@microsoft/microsoft-graph-types';
|
||||||
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||||
@@ -20,7 +19,10 @@ import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsement
|
|||||||
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||||
import { useOidc } from '../../auth/oidcConfig';
|
import { useOidc } from '../../auth/oidcConfig';
|
||||||
import { getOidc } from '../../auth/oidcConfig';
|
import { getOidc } from '../../auth/oidcConfig';
|
||||||
// import httpClient from '../../httpClient/httpClient';
|
import httpClient from '../../httpClient/httpClient';
|
||||||
|
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input } from '@heroui/react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||||
pilotId,
|
pilotId,
|
||||||
@@ -51,7 +53,6 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
});
|
});
|
||||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||||
const [isError, setIsError] = useState<boolean>(false);
|
const [isError, setIsError] = useState<boolean>(false);
|
||||||
const httpClient = useHttpClient();
|
|
||||||
|
|
||||||
const onPeoplePickerSearch = async (
|
const onPeoplePickerSearch = async (
|
||||||
value: string
|
value: string
|
||||||
@@ -151,237 +152,244 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={isDrawerOpen}
|
closeButton={
|
||||||
position='right'
|
<Button isIconOnly>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
placement='right'
|
||||||
data-testid="pilot-drawer"
|
data-testid="pilot-drawer"
|
||||||
width='50%'
|
onClose={onCancel}
|
||||||
|
size='xl'
|
||||||
>
|
>
|
||||||
<FormProvider {...methods}>
|
<DrawerContent>
|
||||||
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
<FormProvider {...methods}>
|
||||||
<div className='grid grid-cols-12 gap-3'>
|
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||||
<div className='col-span-10 self-center'>
|
<DrawerHeader>
|
||||||
<h2 style={{ margin: 0 }}>{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}</h2>
|
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
|
||||||
</div>
|
</DrawerHeader>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<DrawerBody>
|
||||||
<IconButton onClick={onCancel}>
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
<Icon iconName={IconName.XMARK} />
|
<div className='col-span-3 self-center'>
|
||||||
</IconButton>
|
<h6>Name *</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-3 self-center'>
|
<div className='col-span-9'>
|
||||||
<h6>Name *</h6>
|
<PeoplePicker
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<PeoplePicker
|
|
||||||
disabled={isDisabled}
|
|
||||||
// loading={isPeoplePickerLoading}
|
|
||||||
onInputChanged={onPeoplePickerSearch}
|
|
||||||
onPersonSelected={onPersonSelected}
|
|
||||||
people={peoplePickerResults}
|
|
||||||
value={peoplePickerValue}
|
|
||||||
width='w-full'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isUserLoggedIn &&
|
|
||||||
<>
|
|
||||||
<div className='col-span-3 self-center'>
|
|
||||||
<h6>Address *</h6>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<Controller
|
|
||||||
name="address"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: 'An address is required' }}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
disabled={isDisabled}
|
|
||||||
color={methods.formState.errors.address ? 'error' : undefined}
|
|
||||||
helperText={
|
|
||||||
methods.formState.errors.address
|
|
||||||
? methods.formState.errors.address.message
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
width='w-full'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-3 self-center'>
|
|
||||||
<h6>City *</h6>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<Controller
|
|
||||||
name="city"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: 'A city is required' }}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
disabled={isDisabled}
|
|
||||||
color={methods.formState.errors.city ? 'error' : undefined}
|
|
||||||
helperText={
|
|
||||||
methods.formState.errors.city
|
|
||||||
? methods.formState.errors.city.message
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
width='w-full'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-3 self-center'>
|
|
||||||
<h6>State *</h6>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<Controller
|
|
||||||
name="state"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: 'A state must be selected' }}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<StateSelect
|
|
||||||
disabled={isDisabled}
|
|
||||||
// error={methods.formState.errors.state ? true : false}
|
|
||||||
// helperText={
|
|
||||||
// methods.formState.errors.state
|
|
||||||
// ? methods.formState.errors.state.message?.toString()
|
|
||||||
// : undefined
|
|
||||||
// }
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
width='w-full'
|
|
||||||
data-testid="pilot-form-state-dropdown"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-3 self-center'>
|
|
||||||
<h6>Postal Code *</h6>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<Controller
|
|
||||||
name="postalCode"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: 'A postal code is required' }}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
disabled={isDisabled}
|
|
||||||
color={methods.formState.errors.postalCode ? 'error' : undefined}
|
|
||||||
helperText={
|
|
||||||
methods.formState.errors.postalCode
|
|
||||||
? methods.formState.errors.postalCode.message
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
width='w-full'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-3 self-center'>
|
|
||||||
<h6>Email</h6>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<Controller
|
|
||||||
name="email"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{
|
|
||||||
pattern: {
|
|
||||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
||||||
message: 'Invalid email address'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
disabled={isDisabled}
|
|
||||||
color={methods.formState.errors.email ? 'error' : undefined}
|
|
||||||
helperText={
|
|
||||||
methods.formState.errors.email
|
|
||||||
? methods.formState.errors.email.message
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
width='w-full'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-3 self-center'>
|
|
||||||
<h6>Phone Number</h6>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-9'>
|
|
||||||
<Controller
|
|
||||||
name="phone"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{
|
|
||||||
pattern: {
|
|
||||||
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
|
|
||||||
message: 'Enter phone number as 123-456-7890'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
disabled={isDisabled}
|
|
||||||
color={methods.formState.errors.phone ? 'error' : undefined}
|
|
||||||
helperText={
|
|
||||||
methods.formState.errors.phone
|
|
||||||
? methods.formState.errors.phone.message
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
width='w-full'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{/* {isAuthenticated &&
|
|
||||||
<Grid size={12}>
|
|
||||||
<PilotFormMedical
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
}
|
|
||||||
<Grid size={12}>
|
|
||||||
<PilotFormCertificates isDisabled={isDisabled} mode={mode} />
|
|
||||||
</Grid>
|
|
||||||
<Grid size={12}>
|
|
||||||
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
|
||||||
</Grid> */}
|
|
||||||
<div className='col-span-12 justify-self-end self-center'>
|
|
||||||
<Button
|
|
||||||
disabled={
|
|
||||||
isDisabled && mode.toString() !== FormMode.VIEW
|
|
||||||
? isDisabled
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
startContent={<Icon iconName={IconName.XMARK} />}
|
|
||||||
onClick={onCancel}
|
|
||||||
outline={true}
|
|
||||||
data-testid="pilot-cancel-button"
|
|
||||||
>
|
|
||||||
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
|
||||||
</Button>
|
|
||||||
{mode.toString() !== FormMode.VIEW && (
|
|
||||||
<Button
|
|
||||||
color='primary'
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
startContent={<Icon iconName={IconName.SAVE} />}
|
// loading={isPeoplePickerLoading}
|
||||||
type="submit"
|
onInputChanged={onPeoplePickerSearch}
|
||||||
data-testid="pilot-save-button"
|
onPersonSelected={onPersonSelected}
|
||||||
>
|
people={peoplePickerResults}
|
||||||
Save
|
value={peoplePickerValue}
|
||||||
</Button>
|
width='w-full'
|
||||||
)}
|
/>
|
||||||
|
</div>
|
||||||
|
{isUserLoggedIn &&
|
||||||
|
<>
|
||||||
|
<div className='col-span-3 self-center'>
|
||||||
|
<span>Address *</span>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-9'>
|
||||||
|
<Controller
|
||||||
|
name="address"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'An address is required' }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.address ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.address
|
||||||
|
? methods.formState.errors.address.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-3 self-center'>
|
||||||
|
<h6>City *</h6>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-9'>
|
||||||
|
<Controller
|
||||||
|
name="city"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A city is required' }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.city ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.city
|
||||||
|
? methods.formState.errors.city.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-3 self-center'>
|
||||||
|
<h6>State *</h6>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-9'>
|
||||||
|
<Controller
|
||||||
|
name="state"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A state must be selected' }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<StateSelect
|
||||||
|
disabled={isDisabled}
|
||||||
|
// error={methods.formState.errors.state ? true : false}
|
||||||
|
// helperText={
|
||||||
|
// methods.formState.errors.state
|
||||||
|
// ? methods.formState.errors.state.message?.toString()
|
||||||
|
// : undefined
|
||||||
|
// }
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
width='w-full'
|
||||||
|
data-testid="pilot-form-state-dropdown"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-3 self-center'>
|
||||||
|
<h6>Postal Code *</h6>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-9'>
|
||||||
|
<Controller
|
||||||
|
name="postalCode"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A postal code is required' }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.postalCode ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.postalCode
|
||||||
|
? methods.formState.errors.postalCode.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-3 self-center'>
|
||||||
|
<h6>Email</h6>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-9'>
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{
|
||||||
|
pattern: {
|
||||||
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
|
message: 'Invalid email address'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.email ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.email
|
||||||
|
? methods.formState.errors.email.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-3 self-center'>
|
||||||
|
<h6>Phone Number</h6>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-9'>
|
||||||
|
<Controller
|
||||||
|
name="phone"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{
|
||||||
|
pattern: {
|
||||||
|
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
|
||||||
|
message: 'Enter phone number as 123-456-7890'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={isDisabled}
|
||||||
|
color={methods.formState.errors.phone ? 'danger' : undefined}
|
||||||
|
errorMessage={
|
||||||
|
methods.formState.errors.phone
|
||||||
|
? methods.formState.errors.phone.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{/* {isAuthenticated &&
|
||||||
|
<Grid size={12}>
|
||||||
|
<PilotFormMedical
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
<Grid size={12}>
|
||||||
|
<PilotFormCertificates isDisabled={isDisabled} mode={mode} />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
|
||||||
|
</Grid> */}
|
||||||
|
{/* <div className='col-span-12 justify-self-end self-center'> */}
|
||||||
|
|
||||||
|
{/* </div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DrawerBody>
|
||||||
</form>
|
<DrawerFooter>
|
||||||
</FormProvider>
|
<Button
|
||||||
|
disabled={
|
||||||
|
isDisabled && mode.toString() !== FormMode.VIEW
|
||||||
|
? isDisabled
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
startContent={<Icon iconName={IconName.XMARK} />}
|
||||||
|
onPress={onCancel}
|
||||||
|
data-testid="pilot-cancel-button"
|
||||||
|
>
|
||||||
|
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||||
|
</Button>
|
||||||
|
{mode.toString() !== FormMode.VIEW && (
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
disabled={isDisabled}
|
||||||
|
startContent={<Icon iconName={IconName.SAVE} />}
|
||||||
|
type="submit"
|
||||||
|
data-testid="pilot-save-button"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DrawerFooter>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
export interface Pilot {
|
export interface Pilot {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { useEffect, useReducer, useState } from 'react';
|
import { useEffect, useReducer, useState } from 'react';
|
||||||
import PilotForm from '../pilotForm/PilotForm';
|
import PilotForm from '../pilotForm/PilotForm';
|
||||||
import {
|
import {
|
||||||
Alert,
|
// Alert,
|
||||||
Button,
|
// Button,
|
||||||
ColumnDef,
|
// ColumnDef,
|
||||||
|
// Dropdown,
|
||||||
Icon,
|
Icon,
|
||||||
|
IconButton,
|
||||||
IconName,
|
IconName,
|
||||||
Table,
|
// Table,
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
|
||||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Pilot } from './Pilot.interface';
|
import { Pilot } from './Pilot.interface';
|
||||||
@@ -19,34 +20,47 @@ import PilotCard from '../pilotCard/PilotCard';
|
|||||||
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
||||||
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||||
import { UserRole } from '../../enums/userRole';
|
import { UserRole } from '../../enums/userRole';
|
||||||
|
import httpClient from '../../httpClient/httpClient'
|
||||||
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
|
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||||
|
import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faEllipsisVertical, faPen, faEye, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
const Pilots: React.FC<unknown> = () => {
|
const Pilots: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
// const { httpClient } = useHttpClient();
|
||||||
const { isUserLoggedIn, decodedIdToken } = useOidc();
|
const { userRole } = useUserRole();
|
||||||
const userRole = useUserRole();
|
const { screenSize } = useBreakpoints()
|
||||||
|
|
||||||
const getPilots = async () => {
|
const getPilots = async () => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(
|
// const oidc = await getOidc();
|
||||||
`api/pilots`
|
let response: AxiosResponse;
|
||||||
);
|
// if (oidc.isUserLoggedIn) {
|
||||||
console.log(response);
|
// const { accessToken } = await oidc.getTokens();
|
||||||
if (response.data.length > 0) {
|
console.log('blah')
|
||||||
dispatch({ type: 'SET_PILOTS', payload: response.data });
|
response = await httpClient.get(
|
||||||
|
`api/pilots`
|
||||||
|
);
|
||||||
|
|
||||||
if (state.alert) {
|
console.log(response);
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
if (response.data.length > 0) {
|
||||||
|
dispatch({ type: 'SET_PILOTS', payload: response.data });
|
||||||
|
|
||||||
|
if (state.alert) {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }})
|
||||||
}
|
}
|
||||||
} else {
|
// }
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No pilots found.' }})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of pilots failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', message: `Loading of pilots failed with the following message: ${axiosError.message}`}
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
||||||
@@ -105,7 +119,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ALERT',
|
type: 'SET_ALERT',
|
||||||
payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
|
||||||
@@ -119,23 +133,94 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<Pilot>[] = [
|
const columns = [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
id: 'name',
|
||||||
header: 'Name'
|
name: 'Name'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Actions',
|
id: 'actions',
|
||||||
cell: (info: any) => {
|
name: 'Actions'
|
||||||
console.log(info)
|
}
|
||||||
return <ActionMenu
|
]
|
||||||
id={info.row.original.id}
|
|
||||||
onDelete={onDeleteEntry}
|
// const columns: ColumnDef<Pilot>[] = [
|
||||||
onOpenCloseForm={onOpenClosePilotForm}
|
// {
|
||||||
/>
|
// accessorKey: 'name',
|
||||||
|
// header: 'Name'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// header: 'Actions',
|
||||||
|
// cell: (info: any) => {
|
||||||
|
// return (
|
||||||
|
// <Dropdown
|
||||||
|
// options={[
|
||||||
|
// 'Edit',
|
||||||
|
// 'View',
|
||||||
|
// 'Delete'
|
||||||
|
// ]}
|
||||||
|
// onOptionSelected={() => console.log(info.row.original.id)}
|
||||||
|
// >
|
||||||
|
// <IconButton>
|
||||||
|
// <Icon className='text-xl' iconName={IconName.ELLIPSIS_VERTICAL} />
|
||||||
|
// </IconButton>
|
||||||
|
// </Dropdown>
|
||||||
|
// )
|
||||||
|
// // return <ActionMenu
|
||||||
|
// // id={info.row.original.id}
|
||||||
|
// // onDelete={onDeleteEntry}
|
||||||
|
// // onOpenCloseForm={onOpenClosePilotForm}
|
||||||
|
// // />
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
const renderCell = (pilot: any, columnKey: any) => {
|
||||||
|
const cellValue = pilot[columnKey]
|
||||||
|
console.log(cellValue)
|
||||||
|
switch (columnKey) {
|
||||||
|
case 'actions': {
|
||||||
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly variant='light' size='lg'>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownSection showDivider>
|
||||||
|
<DropdownItem
|
||||||
|
key='edit'
|
||||||
|
onPress={() => onOpenClosePilotForm(FormMode.EDIT)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faPen} />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key='view'
|
||||||
|
onPress={() => onOpenClosePilotForm(FormMode.VIEW)}
|
||||||
|
startContent={<FontAwesomeIcon icon={faEye} />}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
<DropdownSection>
|
||||||
|
<DropdownItem
|
||||||
|
key='Delete'
|
||||||
|
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownSection>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return cellValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isFormOpen) {
|
if (!state.isFormOpen) {
|
||||||
@@ -150,7 +235,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
<h1>Pilots</h1>
|
<h1>Pilots</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<div className='col-span-2 justify-self-end self-center'>
|
||||||
{userRole && userRole !== UserRole.READ &&
|
{userRole === UserRole.WRITE &&
|
||||||
<Button
|
<Button
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||||
@@ -167,20 +252,40 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={state.alert.severity}
|
color={state.alert.severity}
|
||||||
>
|
title={state.alert.message}
|
||||||
{state.alert.message}
|
/>
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='col-span-12'>
|
<div className='col-span-12'>
|
||||||
{state.pilots.length > 0 &&
|
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
|
||||||
<Table columns={columns} data={state.pilots} />
|
// <Table columns={columns} data={state.pilots} />
|
||||||
|
<Table>
|
||||||
|
<TableHeader columns={columns}>
|
||||||
|
{(column) => (
|
||||||
|
<TableColumn
|
||||||
|
key={column.id}
|
||||||
|
align={column.id === "actions" ? "center" : "start"}
|
||||||
|
>
|
||||||
|
{column.name}
|
||||||
|
</TableColumn>
|
||||||
|
)}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody items={state.pilots}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
{(columnKey => (
|
||||||
|
<TableCell>
|
||||||
|
{renderCell(item, columnKey)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
}
|
}
|
||||||
{state.pilots.length > 0 &&
|
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<div className='lg:hidden'>
|
<PilotCard pilots={state.pilots} onDelete={onDeleteEntry} onOpenCloseForm={onOpenClosePilotForm} />
|
||||||
<PilotCard pilots={state.pilots} onDelete={onDeleteEntry} onOpenCloseForm={onOpenClosePilotForm} />
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,36 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||||
import {
|
import { AxiosResponse } from 'axios';
|
||||||
Dropdown,
|
|
||||||
Icon,
|
|
||||||
IconName,
|
|
||||||
Navbar,
|
|
||||||
} from '@noahspan/noahspan-components';
|
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
|
||||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
|
||||||
import { User } from '@microsoft/microsoft-graph-types';
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
||||||
|
import { Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarMenuToggle, NavbarMenu, NavbarMenuItem } from '@heroui/react';
|
||||||
|
import httpClient from '../../httpClient/httpClient'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faPlane } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
const SiteNav = () => {
|
const SiteNav = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [userPhoto, setUserPhoto] = useState<string>();
|
const [userPhoto, setUserPhoto] = useState<string>();
|
||||||
const [pages, setPages] = useState<{ name: string; url: string; }[]>([]);
|
const [pages, setPages] = useState<{ name: string; path: string; }[]>([]);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
const { isUserLoggedIn, login, logout } = useOidc()
|
const { isUserLoggedIn, login, logout } = useOidc()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { pathname } = useLocation()
|
||||||
const getPages = () => {
|
const getPages = () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
{
|
{
|
||||||
name: 'Flights',
|
name: 'Flights',
|
||||||
url: '/'
|
path: '/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logbook',
|
name: 'Logbook',
|
||||||
url: '/logbook'
|
path: '/logbook'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Pilots',
|
name: 'Pilots',
|
||||||
url: '/pilots'
|
path: '/pilots'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -85,16 +83,16 @@ const SiteNav = () => {
|
|||||||
navigate(url);
|
navigate(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings = () => {
|
// const Settings = () => {
|
||||||
return (
|
// return (
|
||||||
<div>
|
// <div>
|
||||||
<Icon iconName={IconName.SIGN_OUT} />
|
// <Icon iconName={IconName.SIGN_OUT} />
|
||||||
<span>
|
// <span>
|
||||||
Sign Out
|
// Sign Out
|
||||||
</span>
|
// </span>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setUserProfile = async () => {
|
const setUserProfile = async () => {
|
||||||
@@ -138,17 +136,38 @@ const SiteNav = () => {
|
|||||||
getPages();
|
getPages();
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(pathname)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar isBordered maxWidth='full' position='static'>
|
||||||
authenticated={isUserLoggedIn}
|
<NavbarBrand>
|
||||||
color='base'
|
<img
|
||||||
handlePageClick={handlePageClick}
|
height={35}
|
||||||
handleSignIn={handleSignIn}
|
width={35}
|
||||||
logo={<Icon className='mt-1' iconName={IconName.PLANE} size="2x" />}
|
src='noahspan-logo.png'
|
||||||
pages={pages}
|
style={{ marginRight: '5px' }}
|
||||||
settings={<Settings />}
|
/>
|
||||||
userPhoto={userPhoto}
|
<FontAwesomeIcon icon={faPlane} size='2x' />
|
||||||
/>
|
</NavbarBrand>
|
||||||
|
<NavbarContent justify='center'>
|
||||||
|
{pages.length > 0 && pages.map((page) => {
|
||||||
|
return (
|
||||||
|
<NavbarItem isActive={pathname === page.path ? true : false}>
|
||||||
|
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}>
|
||||||
|
{page.name}
|
||||||
|
</Link>
|
||||||
|
</NavbarItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</NavbarContent>
|
||||||
|
<NavbarContent justify='end'>
|
||||||
|
<Button color='default' onClick={handleSignIn} variant='flat'>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</NavbarContent>
|
||||||
|
</Navbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
224
client/src/components/tracksForm/TracksForm.tsx
Normal file
224
client/src/components/tracksForm/TracksForm.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { useEffect, useReducer } from "react";
|
||||||
|
// import { Button, Drawer, Icon, IconButton, IconName, Input, Loading } from "@noahspan/noahspan-components";
|
||||||
|
import { AxiosError, AxiosInstance, AxiosResponse } from "axios";
|
||||||
|
import { TracksFormProps } from "./TracksFormProps.interface";
|
||||||
|
import { FormMode } from "../../enums/formMode";
|
||||||
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
|
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
||||||
|
import { initialState, reducer } from "./reducer";
|
||||||
|
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
||||||
|
import httpClient from "../../httpClient/httpClient";
|
||||||
|
import { Button, Drawer, DrawerContent, DrawerBody, DrawerHeader, Input, Spinner, DrawerFooter } from '@heroui/react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
|
||||||
|
const TracksForm = () => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState)
|
||||||
|
const logbookContext = useLogbookContext();
|
||||||
|
|
||||||
|
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
console.log('blah')
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_IS_LOADING', payload: true})
|
||||||
|
|
||||||
|
const file = event.target.files![0]
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
httpClient.interceptors.request.use((config) => {
|
||||||
|
config.headers["Content-Type"] = 'multipart/form-data'
|
||||||
|
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadResponse: AxiosResponse = await httpClient.post(`api/tracks/${logbookContext.state.selectedLogId}/1`, formData);
|
||||||
|
// const uploadUrl = uploadResponse.data.url;
|
||||||
|
// const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : [];
|
||||||
|
|
||||||
|
// tracks.push(uploadUrl)
|
||||||
|
// log.tracks = JSON.stringify(tracks);
|
||||||
|
// await httpClient.put(`api/logs/log/${logbookContext.state.selectedLogId}`, log, config);
|
||||||
|
|
||||||
|
// const updatedLog = await getLog();
|
||||||
|
|
||||||
|
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const onDeleteTrack = async (index: number) => {
|
||||||
|
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { index: index }}})
|
||||||
|
// }
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getTracks = async () => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse = await httpClient.get(
|
||||||
|
`api/tracks/${logbookContext.state.selectedLogId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const tracks = response.data;
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_TRACKS', payload: tracks })
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
|
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logbookContext.state.selectedLogId) {
|
||||||
|
getTracks();
|
||||||
|
}
|
||||||
|
}, [logbookContext.state.selectedLogId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-12 gap-3'>
|
||||||
|
<>
|
||||||
|
{state.tracks.length > 0 &&
|
||||||
|
<>
|
||||||
|
<div className='col-span-10'>
|
||||||
|
<Input type='text' />
|
||||||
|
</div>
|
||||||
|
<div className='col-span-2'>
|
||||||
|
<Button isIconOnly onPress={() => console.log('delete')}><FontAwesomeIcon icon={faTrash} /></Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
<div className='col-span-12'>
|
||||||
|
<Button
|
||||||
|
as='label'
|
||||||
|
disabled={state.isLoading ? true : false}
|
||||||
|
fullWidth={true}
|
||||||
|
startContent={<FontAwesomeIcon icon={faUpload} />}
|
||||||
|
>
|
||||||
|
Upload Track
|
||||||
|
<input hidden onChange={handleFileUpload} type='file' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// const getConfig = async () => {
|
||||||
|
// const config = auth.isAuthenticated
|
||||||
|
// ? { headers: { Authorization: auth.user?.access_token } }
|
||||||
|
// : {};
|
||||||
|
|
||||||
|
// return config
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const getLog = async (): Promise<ILogbookEntry> => {
|
||||||
|
// const logResponse: AxiosResponse = await httpClient.get(
|
||||||
|
// `api/tracks/${selectedLogId}`,
|
||||||
|
// await getConfig()
|
||||||
|
// );
|
||||||
|
// const logData: ILogbookEntry = logResponse.data;
|
||||||
|
|
||||||
|
// return logData
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const onCancel = () => {
|
||||||
|
// onOpenClose(FormMode.CANCEL)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const onDeleteTrack = async (fileName: string, index: number) => {
|
||||||
|
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { fileName: fileName, index: index }}})
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const onConfirmDialogConfirm = async () => {
|
||||||
|
// try {
|
||||||
|
// const config = await getConfig();
|
||||||
|
|
||||||
|
// await httpClient.delete(`api/logs/log/${selectedRowKey}/track?fileName=${state.selectedTrack!.fileName}`, config);
|
||||||
|
|
||||||
|
// const log = await getLog();
|
||||||
|
// const tracks: string[] = JSON.parse(log.tracks!);
|
||||||
|
|
||||||
|
// tracks.splice(state.selectedTrack!.index, 1);
|
||||||
|
// log.tracks = JSON.stringify(tracks);
|
||||||
|
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
|
||||||
|
|
||||||
|
// const updatedLog = await getLog();
|
||||||
|
|
||||||
|
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
||||||
|
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log(error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const onConfirmDialogCancel = async () => {
|
||||||
|
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const updateTracks = async () => {
|
||||||
|
// const log = await getLog();
|
||||||
|
|
||||||
|
// dispatch({ type: 'SET_TRACKS', payload: log.tracks! });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// updateTracks();
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className='grid grid-cols-12 gap-3'>
|
||||||
|
// {}
|
||||||
|
// {logbookContext.state.formMode === FormMode.EDIT &&
|
||||||
|
// <>
|
||||||
|
// {state.isLoading &&
|
||||||
|
// <>
|
||||||
|
// <div>
|
||||||
|
// <Spinner size='lg' />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// Loading...
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// {!state.isLoading && state.tracks.length > 0 && state.tracks.map((track, index) => {
|
||||||
|
// const trackSplit = track.url.split('/')
|
||||||
|
// const filename = trackSplit[trackSplit.length - 1];
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// <div>
|
||||||
|
// <Input disabled={true} value={filename} />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <Button isIconOnly onPress={() => onDeleteTrack(filename, index)}><FontAwesomeIcon icon={faTrash} /></Button>
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
|
// )
|
||||||
|
// })}
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
{/* {logbookContext.state.formMode === FormMode.VIEW &&
|
||||||
|
<LogTrackMaps logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
|
||||||
|
} */}
|
||||||
|
{/* {state.isConfirmDialogOpen && (
|
||||||
|
<ConfirmationDialog
|
||||||
|
contentText="Are you sure you want to delete this track?"
|
||||||
|
isLoading={state.isConfirmDialogLoading}
|
||||||
|
isOpen={state.isConfirmDialogOpen}
|
||||||
|
onCancel={onConfirmDialogCancel}
|
||||||
|
onConfirm={onConfirmDialogConfirm}
|
||||||
|
title="Confirm Delete"
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
// </div>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default TracksForm;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FormMode } from "../../enums/formMode";
|
import { FormMode } from "../../enums/formMode";
|
||||||
|
|
||||||
export interface LogTracksProps {
|
export interface TracksFormProps {
|
||||||
isDrawerOpen: boolean;
|
isDrawerOpen: boolean;
|
||||||
mode: FormMode;
|
mode: FormMode;
|
||||||
onOpenClose: (mode: FormMode) => void;
|
onOpenClose: (mode: FormMode) => void;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface LogTracksState {
|
export interface TracksFormState {
|
||||||
isConfirmDialogOpen: boolean;
|
isConfirmDialogOpen: boolean;
|
||||||
isConfirmDialogLoading: boolean;
|
isConfirmDialogLoading: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LogTracksState } from "./LogTracksState.interface";
|
import { TracksFormState } from "./TracksFormState.interface";
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_IS_CONFIRM_DIALOG_OPEN'; payload: boolean }
|
| { type: 'SET_IS_CONFIRM_DIALOG_OPEN'; payload: boolean }
|
||||||
@@ -7,7 +7,7 @@ type Action =
|
|||||||
| { type: 'SET_ON_DELETE_TRACK'; payload: { isConfirmDialogOpen: boolean, selectedTrack: { fileName: string, index: number } }}
|
| { type: 'SET_ON_DELETE_TRACK'; payload: { isConfirmDialogOpen: boolean, selectedTrack: { fileName: string, index: number } }}
|
||||||
| { type: 'SET_TRACKS'; payload: { id: string; order: number; url: string }[] };
|
| { type: 'SET_TRACKS'; payload: { id: string; order: number; url: string }[] };
|
||||||
|
|
||||||
export const initialState: LogTracksState = {
|
export const initialState: TracksFormState = {
|
||||||
isConfirmDialogOpen: false,
|
isConfirmDialogOpen: false,
|
||||||
isConfirmDialogLoading: false,
|
isConfirmDialogLoading: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -15,7 +15,7 @@ export const initialState: LogTracksState = {
|
|||||||
tracks: []
|
tracks: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducer = (state: LogTracksState, action: Action): LogTracksState => {
|
export const reducer = (state: TracksFormState, action: Action): TracksFormState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_IS_CONFIRM_DIALOG_OPEN': {
|
case 'SET_IS_CONFIRM_DIALOG_OPEN': {
|
||||||
return {
|
return {
|
||||||
5
client/src/context/logbookContext/LogbookContext.tsx
Normal file
5
client/src/context/logbookContext/LogbookContext.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Context, createContext } from 'react';
|
||||||
|
import { LogbookContextProps } from './LogbookContextProps.interface';
|
||||||
|
|
||||||
|
export const LogbookContext: Context<LogbookContextProps> =
|
||||||
|
createContext<LogbookContextProps>({} as LogbookContextProps);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Action } from './reducer';
|
||||||
|
import { LogbookContextState } from './LogbookContextState.interface';
|
||||||
|
|
||||||
|
export interface LogbookContextProps {
|
||||||
|
state: LogbookContextState;
|
||||||
|
dispatch: React.Dispatch<Action>;
|
||||||
|
}
|
||||||
35
client/src/context/logbookContext/LogbookContextProvider.tsx
Normal file
35
client/src/context/logbookContext/LogbookContextProvider.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useMemo, useReducer } from 'react';
|
||||||
|
import { LogbookContext } from './LogbookContext';
|
||||||
|
import { LogbookContextProviderProps } from './LogbookContextProviderProps.interface';
|
||||||
|
import { LogbookContextProps } from './LogbookContextProps.interface';
|
||||||
|
import { LogbookContextState } from './LogbookContextState.interface';
|
||||||
|
import { reducer } from './reducer';
|
||||||
|
import { FormMode } from '../../enums/formMode';
|
||||||
|
|
||||||
|
const AppContextProvider: React.FC<LogbookContextProviderProps> = (
|
||||||
|
props: LogbookContextProviderProps
|
||||||
|
) => {
|
||||||
|
const initialState: LogbookContextState = {
|
||||||
|
formAlert: undefined,
|
||||||
|
formMode: FormMode.VIEW,
|
||||||
|
isDrawerOpen: false,
|
||||||
|
isFormDisabled: false,
|
||||||
|
isFormLoading: false,
|
||||||
|
selectedLogId: ''
|
||||||
|
}
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
const contextValue: LogbookContextProps = useMemo(() => {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
dispatch
|
||||||
|
};
|
||||||
|
}, [state, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogbookContext.Provider value={contextValue}>
|
||||||
|
{props.children}
|
||||||
|
</LogbookContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppContextProvider;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface LogbookContextProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { FormMode } from "../../enums/formMode";
|
||||||
|
import { Alert } from "../../interfaces/Alert.interface";
|
||||||
|
|
||||||
|
export interface LogbookContextState {
|
||||||
|
formAlert: Alert | undefined ;
|
||||||
|
formMode: FormMode;
|
||||||
|
isDrawerOpen: boolean;
|
||||||
|
isFormDisabled: boolean;
|
||||||
|
isFormLoading: boolean;
|
||||||
|
selectedLogId: string | undefined;
|
||||||
|
}
|
||||||
67
client/src/context/logbookContext/reducer.ts
Normal file
67
client/src/context/logbookContext/reducer.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { FormMode } from '../../enums/formMode';
|
||||||
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
|
import { LogbookContextState } from './LogbookContextState.interface';
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| { type: 'SET_FORM_ALERT'; payload: Alert | undefined }
|
||||||
|
| { type: 'SET_FORM_MODE'; payload: FormMode }
|
||||||
|
| { type: 'SET_IS_DRAWER_OPEN'; payload: boolean }
|
||||||
|
| { type: 'SET_IS_FORM_DISABLED'; payload: boolean }
|
||||||
|
| { type: 'SET_IS_FORM_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_OPEN_CLOSE_DRAWER'; payload: { formMode: FormMode, isDrawerOpen: boolean, selectedLogId: string | undefined }}
|
||||||
|
| { type: 'SET_SELECTED_LOG_ID'; payload: string }
|
||||||
|
|
||||||
|
export const reducer = (
|
||||||
|
state: LogbookContextState,
|
||||||
|
action: Action
|
||||||
|
): LogbookContextState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_FORM_ALERT': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
formAlert: action.payload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'SET_FORM_MODE': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
formMode: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_IS_DRAWER_OPEN': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isDrawerOpen: action.payload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'SET_IS_FORM_DISABLED': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isFormDisabled: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_IS_FORM_LOADING': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isFormLoading: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_OPEN_CLOSE_DRAWER': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
formMode: action.payload.formMode,
|
||||||
|
isDrawerOpen: action.payload.isDrawerOpen,
|
||||||
|
selectedLogId: action.payload.selectedLogId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_SELECTED_LOG_ID': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedLogId: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
7
client/src/enums/screenSize.ts
Normal file
7
client/src/enums/screenSize.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum ScreenSize {
|
||||||
|
SM,
|
||||||
|
MD,
|
||||||
|
LG,
|
||||||
|
XL,
|
||||||
|
XXL
|
||||||
|
}
|
||||||
3
client/src/hero.ts
Normal file
3
client/src/hero.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// hero.ts
|
||||||
|
import { heroui } from "@heroui/react";
|
||||||
|
export default heroui();
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import axios, { AxiosInstance, CreateAxiosDefaults } from 'axios';
|
|
||||||
|
|
||||||
export const useHttpClient = () => {
|
|
||||||
let config: CreateAxiosDefaults<any> = {
|
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
config = {
|
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
config = {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpClient: AxiosInstance = axios.create(config);
|
|
||||||
|
|
||||||
return httpClient;
|
|
||||||
};
|
|
||||||
11
client/src/hooks/logbookContext/UseLogbookContext.tsx
Normal file
11
client/src/hooks/logbookContext/UseLogbookContext.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { LogbookContext } from '../../context/logbookContext/LogbookContext';
|
||||||
|
|
||||||
|
export const useLogbookContext = () => {
|
||||||
|
const { state, dispatch } = useContext(LogbookContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
dispatch
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,37 +1,30 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useHttpClient } from "../httpClient/UseHttpClient";
|
|
||||||
import { useAuth } from 'react-oidc-context';
|
import { useAuth } from 'react-oidc-context';
|
||||||
import { AxiosInstance, AxiosResponse } from "axios";
|
import { AxiosInstance, AxiosResponse } from "axios";
|
||||||
import { ILogbookEntry } from "../../components/logbook/ILogbookEntry";
|
import { LogbookEntry } from "../../components/logbook/LogbookEntry.interface";
|
||||||
|
import httpClient from '../../httpClient/httpClient'
|
||||||
|
|
||||||
export const useLogs = () => {
|
export const useLogs = () => {
|
||||||
const [logs, setLogs] = useState<ILogbookEntry[]>();
|
const [logs, setLogs] = useState<LogbookEntry[]>();
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [logsLoading, setLogsLoading] = useState<boolean>(false);
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getLogs = async () => {
|
const getLogs = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setLogsLoading(true);
|
||||||
|
|
||||||
const response: AxiosResponse = await httpClient.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`api/logs`,
|
`api/logs`
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: auth.user?.access_token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
const logs: ILogbookEntry[] = response.data;
|
const logs: LogbookEntry[] = response.data;
|
||||||
|
|
||||||
logs.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
logs.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
|
||||||
setLogs(logs)
|
setLogs(logs)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return error;
|
return error;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setLogsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +33,6 @@ export const useLogs = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
logs,
|
logs,
|
||||||
isLoading
|
logsLoading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import httpClient from '../../httpClient/httpClient';
|
||||||
import { useAuth } from 'react-oidc-context';
|
|
||||||
|
|
||||||
export const usePilots = () => {
|
export const usePilots = () => {
|
||||||
const [pilots, setPilots] = useState<any[]>();
|
const [pilots, setPilots] = useState<any[]>();
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
|
||||||
const auth = useAuth();
|
|
||||||
|
|
||||||
const getPilot = async (pilotId: string) => {
|
const getPilot = async (pilotId: string) => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`api/pilots/${pilotId}`,
|
`api/pilots/${pilotId}`
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: auth.user?.access_token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -29,14 +21,9 @@ export const usePilots = () => {
|
|||||||
const getPilots = async () => {
|
const getPilots = async () => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`api/pilots`,
|
`/api/pilots`
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: auth.user?.access_token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
console.log(response)
|
||||||
setPilots(response.data);
|
setPilots(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return error;
|
return error;
|
||||||
|
|||||||
66
client/src/hooks/useBreakpoints/UseBreakpoints.tsx
Normal file
66
client/src/hooks/useBreakpoints/UseBreakpoints.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
|
|
||||||
|
export const useBreakpoints = () => {
|
||||||
|
const [screenSize, setScreenSize] = useState<ScreenSize>();
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
|
||||||
|
const getWindowSize = (width: number): ScreenSize => {
|
||||||
|
let size!: ScreenSize;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case width < 640: {
|
||||||
|
size = ScreenSize.SM;
|
||||||
|
console.log('small')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width >= 640: {
|
||||||
|
size = ScreenSize.MD;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width >= 1024: {
|
||||||
|
size = ScreenSize.LG
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width >= 1280: {
|
||||||
|
size = ScreenSize.XL;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width >= 1536: {
|
||||||
|
size = ScreenSize.XXL;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWindowResize = () => {
|
||||||
|
const width: number = window.innerWidth;
|
||||||
|
const newScreenSize: ScreenSize = getWindowSize(width);
|
||||||
|
console.log(newScreenSize)
|
||||||
|
setScreenSize(newScreenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(windowWidth)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onWindowResize()
|
||||||
|
|
||||||
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onWindowResize);
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenSize
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useOidc } from "../../auth/oidcConfig";
|
import { useOidc } from "../../auth/oidcConfig";
|
||||||
import { UserRole } from "../../enums/userRole";
|
import { UserRole } from "../../enums/userRole";
|
||||||
|
|
||||||
export const useUserRole = (): UserRole | undefined => {
|
export const useUserRole = () => {
|
||||||
const [userRole, setUserRole] = useState<UserRole>()
|
const [userRole, setUserRole] = useState<UserRole>()
|
||||||
const { isUserLoggedIn, decodedIdToken } = useOidc();
|
const { isUserLoggedIn, decodedIdToken } = useOidc();
|
||||||
|
|
||||||
@@ -24,5 +24,7 @@ export const useUserRole = (): UserRole | undefined => {
|
|||||||
}
|
}
|
||||||
}, [decodedIdToken, isUserLoggedIn])
|
}, [decodedIdToken, isUserLoggedIn])
|
||||||
|
|
||||||
return userRole
|
return {
|
||||||
|
userRole
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,10 +12,10 @@ const httpClient: AxiosInstance = axios.create(config)
|
|||||||
|
|
||||||
httpClient.interceptors.request.use(async (config) => {
|
httpClient.interceptors.request.use(async (config) => {
|
||||||
const oidc = await getOidc();
|
const oidc = await getOidc();
|
||||||
console.log('blah')
|
|
||||||
if (oidc.isUserLoggedIn) {
|
if (oidc.isUserLoggedIn) {
|
||||||
const { accessToken } = await oidc.getTokens();
|
const { accessToken } = await oidc.getTokens();
|
||||||
console.log(accessToken)
|
|
||||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@source "../node_modules/@noahspan/noahspan-components/dist/**/*.{js,ts,jsx,tsx}";
|
|
||||||
|
|
||||||
@plugin "daisyui" {
|
|
||||||
themes: lofi --default;
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "@tailwindcss/typography";
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply bg-base-300;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface Alert {
|
export interface Alert {
|
||||||
severity: 'success' | 'error' | 'info' | 'warning';
|
severity: 'danger' | 'default' | 'success' | 'warning';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -2,39 +2,20 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
||||||
|
import LogbookContextProvider from './context/logbookContext/LogbookContextProvider.tsx'
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import './index.css';
|
|
||||||
import { OidcProvider } from './auth/oidcConfig.ts';
|
import { OidcProvider } from './auth/oidcConfig.ts';
|
||||||
// import { AuthProvider as OidcProvider } from 'react-oidc-context';
|
|
||||||
// import { oidcConfig } from './auth/oidcConfig.ts';
|
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
// const msalInstance: PublicClientApplication = new PublicClientApplication(msalConfig);
|
<React.StrictMode>
|
||||||
|
<OidcProvider>
|
||||||
// msalInstance.initialize().then(() => {
|
<AppContextProvider>
|
||||||
// const accounts = msalInstance.getAllAccounts();
|
<LogbookContextProvider>
|
||||||
|
|
||||||
// if (accounts.length > 0) {
|
|
||||||
// msalInstance.setActiveAccount(accounts[0]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// msalInstance.addEventCallback((event: EventMessage) => {
|
|
||||||
// if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
|
|
||||||
// const payload = event.payload as AuthenticationResult;
|
|
||||||
// const account = payload.account;
|
|
||||||
// msalInstance.setActiveAccount(account);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<OidcProvider>
|
|
||||||
<AppContextProvider>
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AppContextProvider>
|
</LogbookContextProvider>
|
||||||
</OidcProvider>
|
</AppContextProvider>
|
||||||
</React.StrictMode>
|
</OidcProvider>
|
||||||
);
|
</React.StrictMode>
|
||||||
// })
|
);
|
||||||
|
|||||||
10
client/src/styles.css
Normal file
10
client/src/styles.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin './hero.ts';
|
||||||
|
/* Note: You may need to change the path to fit your project structure */
|
||||||
|
@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
|
/* body {
|
||||||
|
@apply bg-base-300;
|
||||||
|
} */
|
||||||
10
client/src/tanstack.d.ts
vendored
Normal file
10
client/src/tanstack.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import '@tanstack/react-table';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
declare module '@tanstack/react-table' {
|
||||||
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
headerAlign?: 'left' | 'center' | 'right';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
Binary file not shown.
3515
package-lock.json
generated
3515
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,9 @@
|
|||||||
"tests"
|
"tests"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"daisyui": "^5.1.10"
|
"@heroui/react": "^2.8.5",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"daisyui": "^5.1.10",
|
||||||
|
"framer-motion": "^12.23.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user