migrating to sqlite

This commit is contained in:
2025-10-24 08:31:13 -05:00
parent 05b17878d7
commit 280b222d9a
69 changed files with 6053 additions and 1885 deletions

View File

@@ -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",

View File

@@ -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],

View File

@@ -0,0 +1,5 @@
import { JwtPayload } from "jwt-decode";
export interface CustomJwtPayload extends JwtPayload {
roles: string[];
}

View File

@@ -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));
}
}

View File

@@ -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,

View File

@@ -24,5 +24,5 @@ export class LogDto {
solo?: number; solo?: number;
pilotInCommand?: number; pilotInCommand?: number;
notes?: string; notes?: string;
pilot?: PilotEntity; tracks?: []
} }

View File

@@ -7,6 +7,9 @@ export class LogEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string id: string
@Column()
pilotId: string;
@Column() @Column()
date: Date; date: Date;

View 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;
}
})
);
}
}

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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,

View 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;
}
})
);
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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",

View File

@@ -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>
); );
}; };

View File

@@ -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
})); }));

View File

@@ -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" />

View File

@@ -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

View 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;
}

View 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
}
}
}

View File

@@ -1,8 +0,0 @@
import { FormMode } from '../../enums/formMode';
export interface ILogFormProps {
logId?: string;
isDrawerOpen: boolean;
mode: FormMode;
onOpenClose: (mode: FormMode) => void;
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
import { FormMode } from '../../enums/formMode';
export interface LogFormProps {
logId?: string;
// isDrawerOpen: boolean;
mode: FormMode;
// onOpenClose: (mode: FormMode) => void;
}

View 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;
}

View File

@@ -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': {

View File

@@ -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(() => {

View File

@@ -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;

View File

@@ -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}
/> />
} } */}
</> </>
); );
}; };

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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) : ''
} }
] ]

View File

@@ -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;
} }

View File

@@ -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;

View 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

View File

@@ -0,0 +1,5 @@
import { FormMode } from "../../enums/formMode";
export interface LogbookDrawerProps {
onOpenClose: (mode: FormMode) => void;
}

View File

@@ -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>
); );
}; };

View File

@@ -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;
}; };

View File

@@ -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>

View File

@@ -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>
); );
}; };

View 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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
export interface LogTracksState { export interface TracksFormState {
isConfirmDialogOpen: boolean; isConfirmDialogOpen: boolean;
isConfirmDialogLoading: boolean; isConfirmDialogLoading: boolean;
isLoading: boolean; isLoading: boolean;

View File

@@ -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 {

View File

@@ -0,0 +1,5 @@
import { Context, createContext } from 'react';
import { LogbookContextProps } from './LogbookContextProps.interface';
export const LogbookContext: Context<LogbookContextProps> =
createContext<LogbookContextProps>({} as LogbookContextProps);

View File

@@ -0,0 +1,7 @@
import { Action } from './reducer';
import { LogbookContextState } from './LogbookContextState.interface';
export interface LogbookContextProps {
state: LogbookContextState;
dispatch: React.Dispatch<Action>;
}

View 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;

View File

@@ -0,0 +1,3 @@
export interface LogbookContextProviderProps {
children: React.ReactNode;
}

View File

@@ -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;
}

View 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;
}
}
};

View File

@@ -0,0 +1,7 @@
export enum ScreenSize {
SM,
MD,
LG,
XL,
XXL
}

3
client/src/hero.ts Normal file
View File

@@ -0,0 +1,3 @@
// hero.ts
import { heroui } from "@heroui/react";
export default heroui();

View File

@@ -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;
};

View 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
};
};

View File

@@ -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
} }
} }

View File

@@ -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;

View 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
}
}

View File

@@ -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
}
} }

View File

@@ -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}`;
} }

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
export interface Alert { export interface Alert {
severity: 'success' | 'error' | 'info' | 'warning'; severity: 'danger' | 'default' | 'success' | 'warning';
message: string; message: string;
} }

View File

@@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }
} }