104 switch to daisyui #106

Merged
noahspannbauer merged 6 commits from 104-switch-to-daisyui into main 2025-12-29 20:43:27 -05:00
41 changed files with 1024 additions and 4286 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -35,7 +35,7 @@
"@nestjs/serve-static": "^5.0.3", "@nestjs/serve-static": "^5.0.3",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"@noahspan/azure-database": "^3.1.2", "@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.2.9", "@noahspan/noahspan-modules": "^1.2.11",
"@schematics/angular": "^17.3.7", "@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",

View File

@@ -32,17 +32,18 @@ import { join } from 'path';
}), }),
HealthModule, HealthModule,
LogModule, LogModule,
// MsGraphModule.registerAsync({ MsGraphModule.registerAsync({
// inject: [ConfigService], inject: [ConfigService],
// imports: [ConfigModule], imports: [ConfigModule],
// useFactory: async (configService: ConfigService) => { useFactory: async (configService: ConfigService) => {
// return { return {
// clientId: configService.get<string>('clientId'), authority: configService.get<string>('authority'),
// clientSecret: configService.get<string>('clientSecret'), clientId: configService.get<string>('clientId'),
// tenantId: configService.get<string>('tenantId') clientSecret: configService.get<string>('clientSecret'),
// } tenantId: configService.get<string>('tenantId')
// } }
// }), }
}),
PilotModule, PilotModule,
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join(__dirname, '../..', 'client', 'dist') rootPath: join(__dirname, '../..', 'client', 'dist')

View File

@@ -1,6 +1,7 @@
export default () => ({ export default () => ({
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
audience: process.env.AUDIENCE, audience: process.env.AUDIENCE,
authority: process.env.AUTHORITY,
clientId: process.env.CLIENT_ID, clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET, clientSecret: process.env.CLIENT_SECRET,
issuer: process.env.ISSUER_URL, issuer: process.env.ISSUER_URL,

View File

@@ -65,7 +65,7 @@ export class LogController {
return await this.logService.create(logDto); return await this.logService.create(logDto);
} catch (error) { } catch (error) {
const customError = error as CustomError; const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode); throw new HttpException(customError.message, customError.statusCode);
} }
} }

View File

@@ -10,15 +10,15 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.enableCors({ app.enableCors({
origin: 'http://localhost:8080', // Allow requests from your frontend's origin origin: 'http://localhost:8080',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true, // If you need to send cookies or authorization headers credentials: true,
}); });
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalFilters(new HttpExceptionFilter());
app.use( app.use(
session({ session({
secret: 'blah', secret: process.env.SESSION_SECRET,
resave: false, resave: false,
saveUninitialized: false saveUninitialized: false
}) })

View File

@@ -29,6 +29,7 @@ export class PilotInterceptor implements NestInterceptor {
if (req.headers.authorization) { if (req.headers.authorization) {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1]; const token = authHeader && authHeader.split(' ')[1];
const jwtPayload = jwtDecode(token); const jwtPayload = jwtDecode(token);
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles')); const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));

View File

@@ -14,14 +14,12 @@
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.0", "@fortawesome/react-fontawesome": "^3.1.0",
"@heroui/react": "^2.8.5",
"@noahspan/noahspan-components": "^2.0.0-alpha-14", "@noahspan/noahspan-components": "^2.0.0-alpha-14",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.13",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"axios": "^1.7.2", "axios": "^1.7.2",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"framer-motion": "^12.23.24",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"oidc-spa": "^7.2.4", "oidc-spa": "^7.2.4",
"react": "^19.1.1", "react": "^19.1.1",

View File

@@ -1,23 +1,22 @@
import { Navigate, Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import Flights from './components/flights/Flights'; import Flights from './components/flights/Flights';
import Logbook from './components/logbook/Logbook'; import Logbook from './components/logbook/Logbook';
import Pilots from './components/pilots/Pilots'; import Pilots from './components/pilots/Pilots';
import SiteNav from './components/siteNav/SiteNav'; import SiteNav from './components/siteNav/SiteNav';
import { HeroUIProvider } from '@heroui/react'; import { useNavigate } from 'react-router-dom';
import { useHref, useNavigate } from 'react-router-dom';
const App = () => { const App = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<HeroUIProvider navigate={navigate} useHref={useHref}> <div className='bg-[#f5f5f5]' data-theme="lofi">
<SiteNav /> <SiteNav />
<Routes> <Routes>
<Route path='/' element={<Flights />} /> <Route path='/' element={<Flights />} />
<Route path="/logbook" element={<Logbook />} /> <Route path="/logbook" element={<Logbook />} />
<Route path="/pilots" element={<Pilots />} /> <Route path="/pilots" element={<Pilots />} />
</Routes> </Routes>
</HeroUIProvider> </div>
); );
}; };

View File

@@ -4,10 +4,8 @@ export const { OidcProvider, useOidc, getOidc } = createReactOidc(async () => ({
issuerUri: import.meta.env.VITE_ISSUER_URI, issuerUri: import.meta.env.VITE_ISSUER_URI,
clientId: import.meta.env.VITE_CLIENT_ID, clientId: import.meta.env.VITE_CLIENT_ID,
homeUrl: import.meta.env.VITE_BASE_URL, homeUrl: import.meta.env.VITE_BASE_URL,
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
autoLogin: false, autoLogin: false,
postLoginRedirectUrl: '/', postLoginRedirectUrl: '/',
noIframe: true, noIframe: true,
extraQueryParams: {
audience: "api://flying-test-api"
}
})); }));

View File

@@ -0,0 +1,44 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertProps } from "./AlertProps.interface";
import { faCircleCheck, faCircleInfo, faCircleXmark, faTriangleExclamation, faXmark } from "@fortawesome/free-solid-svg-icons";
const Alert = ({
children,
className,
closeIcon,
severity,
onClose,
...rest
}: AlertProps) => {
const severityVariants = {
info: 'alert-info',
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning'
};
return (
<>
<div
role='alert'
className={`alert ${severity ? severityVariants[severity] : ''} ${className ? className : ''}`}
{...rest}
>
<span>
{severity === 'info' && <FontAwesomeIcon icon={faCircleInfo} />}
{severity === 'error' && <FontAwesomeIcon icon={faCircleXmark} />}
{severity === 'success' && (
<FontAwesomeIcon icon={faCircleCheck} />
)}
{severity === 'warning' && (
<FontAwesomeIcon icon={faTriangleExclamation} />
)}
</span>
<span>{children}</span>
{closeIcon && <button className='btn' onClick={onClose}>{<FontAwesomeIcon icon={faXmark} />}</button>}
</div>
</>
);
};
export default Alert;

View File

@@ -0,0 +1,7 @@
export interface AlertProps {
children?: React.ReactNode;
className?: string;
closeIcon?: React.ReactNode;
onClose?: () => void;
severity: 'info' | 'error' | 'success' | 'warning';
}

View File

@@ -7,7 +7,6 @@
// IconName, // IconName,
// Loading // Loading
// } from '@noahspan/noahspan-components'; // } from '@noahspan/noahspan-components';
import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalFooter, Spinner } from '@heroui/react'
import { DialogConfirmationProps } from './ConfirmationDialogProps.interface'; import { DialogConfirmationProps } from './ConfirmationDialogProps.interface';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
@@ -21,31 +20,25 @@ const ConfirmationDialog = ({
title title
}: DialogConfirmationProps) => { }: DialogConfirmationProps) => {
return ( return (
<Modal <>
isDismissable={false} <input type='checkbox' id='dialog' className='modal-toggle' onChange={() => {}} checked={isOpen} />
isKeyboardDismissDisabled={true} <div className='modal' role='dialog'>
isOpen={isOpen} <div className='modal-box'>
> <h3 className="text-lg font-bold">{title}</h3>
<ModalContent> <p className="py-4">{contentText}</p>
<ModalHeader>{title}</ModalHeader> <div className='modal-action'>
<ModalBody> <button className='btn' onClick={onCancel}>
{!isLoading && <div>{contentText}</div>} <FontAwesomeIcon icon={faXmark} />
{isLoading && <Spinner size='lg' />} No
</ModalBody> </button>
<ModalFooter> <button className='btn btn-primary' onClick={onConfirm}>
<Button onPress={onCancel} startContent={<FontAwesomeIcon icon={faXmark} />}> <FontAwesomeIcon icon={faCircleCheck} />
No Yes
</Button> </button>
<Button </div>
color='primary' </div>
onPress={onConfirm} </div>
startContent={<FontAwesomeIcon icon={faCircleCheck} />} </>
>
Yes
</Button>
</ModalFooter>
</ModalContent>
</Modal>
); );
}; };

View File

@@ -1,11 +1,9 @@
import { Icon, IconName, Skeleton } from "@noahspan/noahspan-components";
import { Card } from '@heroui/react';
import LogbookCard from "../logbookCard/LogbookCard"; import LogbookCard from "../logbookCard/LogbookCard";
import { useEffect, useReducer } from "react"; import { useEffect, useReducer } from "react";
import { useLogs } from "../../hooks/logs/UseLogs"; import { useLogs } from "../../hooks/logs/UseLogs";
import { LogbookEntry } from "../logbook/LogbookEntry.interface"; import { LogbookEntry } from "../logbook/LogbookEntry.interface";
import { initialState, reducer } from "./reducer"; import { initialState, reducer } from "./reducer";
import { Alert } from '@heroui/react' import Alert from "../alert/Alert";
const Flights = () => { const Flights = () => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
@@ -23,7 +21,7 @@ const Flights = () => {
dispatch({ type: 'SET_FLIGHTS', payload: flights}) dispatch({ type: 'SET_FLIGHTS', payload: flights})
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_ALERT', payload: undefined })
} else { } else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No flights found' }}) dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }})
} }
}, [logs]) }, [logs])
@@ -35,12 +33,14 @@ const Flights = () => {
{!logsLoading && state.alert && ( {!logsLoading && state.alert && (
<div> <div>
<Alert <Alert
className='mb-5'
onClose={() => onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_ALERT', payload: undefined })
} }
color={state.alert.severity} severity={state.alert.severity}
title={state.alert.message} >
/> {state.alert.message}
</Alert>
</div> </div>
)} )}
{!logsLoading && {!logsLoading &&
@@ -48,7 +48,7 @@ const Flights = () => {
<LogbookCard logs={state.flights} mode='flights' /> <LogbookCard logs={state.flights} mode='flights' />
</div> </div>
} }
{logsLoading && [...Array(6)].map((_element, index) => { {/* {logsLoading && [...Array(6)].map((_element, index) => {
return ( return (
<div className='mb-5'> <div className='mb-5'>
<Card <Card
@@ -70,7 +70,7 @@ const Flights = () => {
</Card> </Card>
</div> </div>
) )
})} })} */}
</div> </div>
) )
} }

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useReducer } from 'react'; import { useEffect, useReducer } from 'react';
import { Alert, Button, DatePicker, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Input, NumberInput, Select, Selection, SelectItem, SharedSelection, Textarea } from '@heroui/react' import { useForm, Controller, useFormContext } from 'react-hook-form';
import { useForm, Controller, FormProvider, useFormContext } from 'react-hook-form';
import { LogFormProps } from './LogFormProps.interface'; import { LogFormProps } from './LogFormProps.interface';
import { initialState, reducer } from './reducer'; import { initialState, reducer } from './reducer';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
@@ -10,7 +9,6 @@ import { useOidc } from '../../auth/oidcConfig';
import httpClient from '../../httpClient/httpClient'; import httpClient from '../../httpClient/httpClient';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons' import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons'
import { parseAbsolute, parseDate, getLocalTimeZone, CalendarDate, ZonedDateTime } from '@internationalized/date';
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext'; import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
const LogForm = () => { const LogForm = () => {
@@ -40,7 +38,7 @@ const LogForm = () => {
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
dispatch({ type: 'SET_ALERT', payload: { severity: 'danger', message: axiosError.message }}); dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
} finally { } finally {
dispatch({ type: 'SET_IS_LOADING', payload: false }); dispatch({ type: 'SET_IS_LOADING', payload: false });
} }
@@ -59,7 +57,7 @@ const LogForm = () => {
label: pilot.name, label: pilot.name,
}; };
}); });
console.log(newPilotsOptions)
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions }); dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
} }
}, [pilots]); }, [pilots]);
@@ -73,25 +71,21 @@ const LogForm = () => {
<Controller <Controller
name="pilotId" name="pilotId"
control={control} control={control}
render={({ field: { value } }) => { render={({ field: { onChange, value } }) => {
return ( return (
<Select <select
className='select w-full'
aria-labelledby='pilot' aria-labelledby='pilot'
isDisabled={state.isDisabled} disabled={state.isDisabled}
fullWidth={true} onChange={onChange}
isRequired={true} value={[value]}
onSelectionChange={(keys: SharedSelection) => {
setValue('pilotId', keys.currentKey);
}}
selectedKeys={[value]}
size='lg'
> >
{state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => { {state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => {
return ( return (
<SelectItem key={pilotOption.key}>{pilotOption.label}</SelectItem> <option key={pilotOption.key}>{pilotOption.label}</option>
) )
})} })}
</Select> </select>
); );
}} }}
/> />
@@ -104,21 +98,14 @@ const LogForm = () => {
name="date" name="date"
control={control} control={control}
render={({ field: { onChange, value } }) => { render={({ field: { onChange, value } }) => {
const parsedAbsoluteDate = value ? parseAbsolute(value, getLocalTimeZone()) : value
return( return(
<DatePicker <input
aria-labelledby='date' type='date'
isDisabled={state.isDisabled} className='input w-full'
isRequired={true} disabled={state.isDisabled}
onChange={(selectedDate) => { onChange={onChange}
let date = selectedDate as CalendarDate; value={value ? value.split('T')[0] : ''}
/>
setValue('date', date.toDate(getLocalTimeZone()).toISOString())
}}
size='lg'
value={parsedAbsoluteDate ? new CalendarDate(parsedAbsoluteDate.year, parsedAbsoluteDate.month, parsedAbsoluteDate.day) : value}
/>
) )
}} }}
/> />
@@ -131,18 +118,11 @@ const LogForm = () => {
name="aircraftMakeModel" name="aircraftMakeModel"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Input <input
aria-labelledby='aircraftMakeModel' type='text'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isRequired={true}
onChange={onChange} onChange={onChange}
size='lg'
value={value} value={value}
/> />
)} )}
@@ -158,18 +138,11 @@ const LogForm = () => {
name="aircraftIdentity" name="aircraftIdentity"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Input <input
aria-labelledby='aircraftIdentity' type='text'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isRequired={true}
onChange={onChange} onChange={onChange}
size='lg'
value={value} value={value}
/> />
)} )}
@@ -185,18 +158,11 @@ const LogForm = () => {
name="routeFrom" name="routeFrom"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Input <input
aria-labelledby='routeFrom' type='text'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isRequired={true}
onChange={onChange} onChange={onChange}
size='lg'
value={value} value={value}
/> />
)} )}
@@ -210,18 +176,11 @@ const LogForm = () => {
name="routeTo" name="routeTo"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Input <input
aria-labelledby='routeTo' type='text'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isRequired={true}
onChange={onChange} onChange={onChange}
size='lg'
value={value} value={value}
/> />
)} )}
@@ -235,21 +194,11 @@ const LogForm = () => {
name="durationOfFlight" name="durationOfFlight"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='durationOfFlight' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isRequired={true}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -265,21 +214,11 @@ const LogForm = () => {
name="singleEngineLand" name="singleEngineLand"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='singleEngineLand' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isRequired={true}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -293,20 +232,11 @@ const LogForm = () => {
name="simulatorAtd" name="simulatorAtd"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='simulaterAtd' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={formState.errors.address ? 'danger' : undefined} disabled={state.isDisabled}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -323,22 +253,11 @@ const LogForm = () => {
name="landingsDay" name="landingsDay"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='landingsDay' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -352,22 +271,11 @@ const LogForm = () => {
name="landingsNight" name="landingsNight"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='landingsNight' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -386,24 +294,12 @@ const LogForm = () => {
name="groundTrainingReceived" name="groundTrainingReceived"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='groundTrainingReceived' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
width='w-full'
/> />
)} )}
/> />
@@ -418,22 +314,11 @@ const LogForm = () => {
name="flightTrainingReceived" name="flightTrainingReceived"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='flightTrainingReceived' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -447,22 +332,11 @@ const LogForm = () => {
name="crossCountry" name="crossCountry"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='crossCountry' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -476,22 +350,11 @@ const LogForm = () => {
name="night" name="night"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='night' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -505,22 +368,11 @@ const LogForm = () => {
name="solo" name="solo"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='solo' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -534,22 +386,11 @@ const LogForm = () => {
name="pilotInCommand" name="pilotInCommand"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='pilotInCommand' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -566,22 +407,11 @@ const LogForm = () => {
name="instrumentActual" name="instrumentActual"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='instrumentActual' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -595,22 +425,11 @@ const LogForm = () => {
name="instrumentSimulated" name="instrumentSimulated"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='instrumentSimulated' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -626,22 +445,11 @@ const LogForm = () => {
name="instrumentApproaches" name="instrumentApproaches"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='instrumentApproaches' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -655,22 +463,11 @@ const LogForm = () => {
name="instrumentHolds" name="instrumentHolds"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='instrumentHolds' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -684,23 +481,11 @@ const LogForm = () => {
name="instrumentNavTrack" name="instrumentNavTrack"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<NumberInput <input
aria-labelledby='instrumentNavTrack' type='number'
isDisabled={state.isDisabled} className='input w-full'
color={ disabled={state.isDisabled}
formState.errors.address ? 'danger' : undefined
}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
fullWidth={true}
isWheelDisabled={state.isDisabled}
onChange={onChange} onChange={onChange}
radius='lg'
size='sm'
type="number"
value={value} value={value}
/> />
)} )}
@@ -716,19 +501,11 @@ const LogForm = () => {
name="notes" name="notes"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Textarea <textarea
aria-labelledby='notes' className='textarea w-full'
isDisabled={state.isDisabled} disabled={state.isDisabled}
color={formState.errors.address ? 'danger' : undefined}
errorMessage={
formState.errors.address
? formState.errors.address.message?.toString()
: undefined
}
fullWidth={true}
onChange={onChange} onChange={onChange}
value={value} ></textarea>
/>
)} )}
/> />
</div> </div>

View File

@@ -1,13 +1,9 @@
import { Selection } from "@heroui/react";
import { Alert } from "../../interfaces/Alert.interface"; import { Alert } from "../../interfaces/Alert.interface";
export interface LogFormState { export interface LogFormState {
alert: Alert | undefined; alert: Alert | undefined;
experienceSelectedKeys: Selection;
instrumentSelectedKeys: Selection;
isDisabled: boolean; isDisabled: boolean;
isLoading: boolean; isLoading: boolean;
landingsSelectedKeys: Selection;
pilotOptions: { key: string; label: string; }[]; pilotOptions: { key: string; label: string; }[];
selectedPilotName: string; selectedPilotName: string;
} }

View File

@@ -1,24 +1,17 @@
import { Alert } from '../../interfaces/Alert.interface'; import { Alert } from '../../interfaces/Alert.interface';
import { LogFormState } from './LogFormState.interface'; import { LogFormState } from './LogFormState.interface';
import { Selection } from '@heroui/react';
type Action = type Action =
| { type: 'SET_ALERT'; payload: Alert | undefined } | { type: 'SET_ALERT'; payload: Alert | undefined }
| { type: 'SET_EXPERIENCE_SELECTED_KEYS'; payload: Selection }
| { type: 'SET_INSTRUMENT_SELECTED_KEYS'; payload: Selection }
| { type: 'SET_IS_DISABLED'; payload: boolean } | { type: 'SET_IS_DISABLED'; payload: boolean }
| { type: 'SET_IS_LOADING'; payload: boolean } | { type: 'SET_IS_LOADING'; payload: boolean }
| { type: 'SET_LANDINGS_SELECTED_KEYS'; payload: Selection }
| { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] } | { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] }
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string }; | { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
export const initialState: LogFormState = { export const initialState: LogFormState = {
alert: undefined, alert: undefined,
experienceSelectedKeys: new Set([]),
instrumentSelectedKeys: new Set([]),
isDisabled: false, isDisabled: false,
isLoading: true, isLoading: true,
landingsSelectedKeys: new Set(['1']),
pilotOptions: [], pilotOptions: [],
selectedPilotName: '' selectedPilotName: ''
}; };
@@ -34,36 +27,18 @@ export const reducer = (
alert: action.payload alert: action.payload
}; };
} }
case 'SET_EXPERIENCE_SELECTED_KEYS': {
return {
...state,
experienceSelectedKeys: action.payload
}
}
case 'SET_IS_DISABLED': { case 'SET_IS_DISABLED': {
return { return {
...state, ...state,
isDisabled: action.payload isDisabled: action.payload
}; };
} }
case 'SET_INSTRUMENT_SELECTED_KEYS': {
return {
...state,
instrumentSelectedKeys: action.payload
}
}
case 'SET_IS_LOADING': { case 'SET_IS_LOADING': {
return { return {
...state, ...state,
isLoading: action.payload isLoading: action.payload
}; };
} }
case 'SET_LANDINGS_SELECTED_KEYS': {
return {
...state,
landingsSelectedKeys: action.payload
}
}
case 'SET_PILOT_OPTIONS': { case 'SET_PILOT_OPTIONS': {
return { return {
...state, ...state,

View File

@@ -1,10 +1,8 @@
import { Key, useEffect, useReducer } from 'react'; import { useEffect, useReducer } from 'react';
import LogForm from '../logForm/LogForm';
import { initialState, reducer } from './reducer'; import { initialState, reducer } from './reducer';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { FormMode } from '../../enums/formMode'; import { FormMode } from '../../enums/formMode';
import { authColumns, unauthColumns } from './columns'; import { authColumns, unauthColumns } from './columns';
import ActionMenu from '../actionMenu/ActionMenu';
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog'; import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
import { LogbookEntry } from './LogbookEntry.interface'; import { LogbookEntry } from './LogbookEntry.interface';
import LogbookCard from '../logbookCard/LogbookCard'; import LogbookCard from '../logbookCard/LogbookCard';
@@ -14,12 +12,12 @@ import { useUserRole } from '../../hooks/userRole/UseUserRole';
import { UserRole } from '../../enums/userRole'; import { UserRole } from '../../enums/userRole';
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints'; import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
import { ScreenSize } from '../../enums/screenSize'; import { ScreenSize } from '../../enums/screenSize';
import { Table, TableHeader, TableBody, TableColumn, Dropdown, DropdownTrigger, Button, DropdownSection, DropdownMenu, DropdownItem, Alert, TableRow, TableCell } from '@heroui/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons' import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table'; import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table';
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext'; import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
import LogbookDrawer from '../logbookDrawer/LogbookDrawer'; import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
import Alert from '../alert/Alert';
const Logbook: React.FC<unknown> = () => { const Logbook: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
@@ -28,7 +26,6 @@ const Logbook: React.FC<unknown> = () => {
const { userRole } = useUserRole(); const { userRole } = useUserRole();
const { screenSize } = useBreakpoints(); const { screenSize } = useBreakpoints();
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => { const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
const blah = info.table
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value)); const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
let total: number = 0; let total: number = 0;
@@ -69,13 +66,17 @@ const Logbook: React.FC<unknown> = () => {
id: 'route', id: 'route',
header: 'Route of Flight', header: 'Route of Flight',
meta: { meta: {
headerAlign: 'center' className: 'border-l border-base-300',
headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
id: 'routeFrom', id: 'routeFrom',
accessorKey: 'routeFrom', accessorKey: 'routeFrom',
header: 'From' header: 'From',
meta: {
className: 'border-l border-base-300',
}
}, },
{ {
id: 'routeTo', id: 'routeTo',
@@ -89,8 +90,9 @@ const Logbook: React.FC<unknown> = () => {
accessorKey: 'durationOfFlight', accessorKey: 'durationOfFlight',
header: 'Duration Of Flight', header: 'Duration Of Flight',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' className: 'border-l border-base-300',
headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '', info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
@@ -99,51 +101,33 @@ const Logbook: React.FC<unknown> = () => {
const notes: ColumnDef<LogbookEntry> = { const notes: ColumnDef<LogbookEntry> = {
id: 'notes', id: 'notes',
accessorKey: 'notes', accessorKey: 'notes',
header: 'Notes' header: 'Notes',
meta: {
className: 'border-l border-base-300',
}
} }
const actions: ColumnDef<LogbookEntry> = { const actions: ColumnDef<LogbookEntry> = {
id: 'actions', id: 'actions',
header: 'Actions', header: 'Actions',
meta: { meta: {
align: 'center', align: 'text-center',
headerAlign: 'center' className: 'border-l border-base-300',
headerAlign: 'text-center'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => { cell: (info: CellContext<LogbookEntry, unknown>) => {
return ( return (
<Dropdown> <div className='dropdown dropdown-end'>
<DropdownTrigger> <div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
<Button isIconOnly variant='light' size='lg'> <ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300">
<FontAwesomeIcon icon={faEllipsisVertical} /> {isUserLoggedIn && userRole === UserRole.WRITE &&
</Button> <li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
</DropdownTrigger> }
<DropdownMenu> <li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
<DropdownSection showDivider> {isUserLoggedIn && userRole === UserRole.WRITE &&
<DropdownItem <li><a onClick={() => onDeleteLog(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
key='edit' }
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)} </ul>
startContent={<FontAwesomeIcon icon={faPen} />} </div>
>
Edit
</DropdownItem>
<DropdownItem
key='view'
onPress={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}
startContent={<FontAwesomeIcon icon={faEye} />}
>
View
</DropdownItem>
</DropdownSection>
<DropdownSection>
<DropdownItem
key='Delete'
onPress={() => onDeleteLog(info.row.original.id)}
startContent={<FontAwesomeIcon icon={faTrash} />}
>
Delete
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
) )
} }
} }
@@ -173,8 +157,8 @@ const Logbook: React.FC<unknown> = () => {
accessorKey: 'singleEngineLand', accessorKey: 'singleEngineLand',
header: 'Single Engine Land', header: 'Single Engine Land',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '', info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
@@ -184,7 +168,8 @@ const Logbook: React.FC<unknown> = () => {
id: 'landings', id: 'landings',
header: 'Landings', header: 'Landings',
meta: { meta: {
headerAlign: 'center' className: 'border-l border-base-300',
headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
@@ -193,8 +178,9 @@ const Logbook: React.FC<unknown> = () => {
header: 'Day', header: 'Day',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' className: 'border-l border-base-300',
headerAlign: 'text-right'
} }
}, },
{ {
@@ -203,8 +189,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Night', header: 'Night',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
} }
] ]
@@ -213,7 +199,8 @@ const Logbook: React.FC<unknown> = () => {
id: 'instrument', id: 'instrument',
header: 'Instrument', header: 'Instrument',
meta: { meta: {
headerAlign: 'center' className: 'border-l border-base-300',
headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
@@ -222,8 +209,9 @@ const Logbook: React.FC<unknown> = () => {
header: 'Actual', header: 'Actual',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' className: 'border-l border-base-300',
headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '', info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
@@ -234,8 +222,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Simulated', header: 'Simulated',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -246,8 +234,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Approaches', header: 'Approaches',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
}, },
{ {
@@ -256,8 +244,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Holds', header: 'Holds',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
}, },
{ {
@@ -266,8 +254,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Nav/Track', header: 'Nav/Track',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
} }
] ]
@@ -276,7 +264,8 @@ const Logbook: React.FC<unknown> = () => {
id: 'experienceTraining', id: 'experienceTraining',
header: 'Type of pilot experience or training', header: 'Type of pilot experience or training',
meta: { meta: {
headerAlign: 'center' className: 'border-l border-base-300',
headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
@@ -285,8 +274,9 @@ const Logbook: React.FC<unknown> = () => {
header: 'Ground Training Received', header: 'Ground Training Received',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' className: 'border-l border-base-300',
headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -297,8 +287,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Flight Training Received', header: 'Flight Training Received',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -309,8 +299,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Cross Country', header: 'Cross Country',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -321,8 +311,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Night', header: 'Night',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -333,8 +323,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Solo', header: 'Solo',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -345,8 +335,8 @@ const Logbook: React.FC<unknown> = () => {
header: 'Pilot In Command', header: 'Pilot In Command',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -372,14 +362,14 @@ 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: 'default', message: 'No logbook entries found.'}}) dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No logbook entries found.'}})
} }
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
dispatch({ dispatch({
type: 'SET_ALERT', type: 'SET_ALERT',
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`} payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
}); });
} finally { } finally {
dispatch({ type: 'SET_IS_LOADING', payload: false }); dispatch({ type: 'SET_IS_LOADING', payload: false });
@@ -437,7 +427,7 @@ const Logbook: React.FC<unknown> = () => {
dispatch({ dispatch({
type: 'SET_ALERT', type: 'SET_ALERT',
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`} payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
}); });
} finally { } finally {
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false }); dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
@@ -503,44 +493,52 @@ const Logbook: React.FC<unknown> = () => {
</div> </div>
<div className='col-span-2 justify-self-end self-center'> <div className='col-span-2 justify-self-end self-center'>
{userRole === UserRole.WRITE && {userRole === UserRole.WRITE &&
<Button // <Button
color='primary' // color='primary'
onPress={() => onOpenCloseDrawer(FormMode.ADD)} // onPress={() => onOpenCloseDrawer(FormMode.ADD)}
startContent={<FontAwesomeIcon icon={faAdd} />} // startContent={<FontAwesomeIcon icon={faAdd} />}
data-testid="pilot-add-button" // data-testid="pilot-add-button"
// >
// Add Entry
// </Button>
<button className='btn btn-primary'
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
> >
<FontAwesomeIcon icon={faAdd} />
Add Entry Add Entry
</Button> </button>
} }
</div> </div>
{!state.isLoading && state.alert && ( {!state.isLoading && state.alert && (
<div className='col-span-12 mb-5'> <div className='col-span-12 mb-5'>
<Alert <Alert
className='mb-5'
onClose={() => onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_ALERT', payload: undefined })
} }
color={'default'} severity={'info'}
title={state.alert.message} >
/> {state.alert.message}
</Alert>
</div> </div>
)} )}
{!state.isLoading && ( {!state.isLoading && (
<div className='col-span-12'> <div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
{state.entries.length > 0 && screenSize !== ScreenSize.SM && ( {state.entries.length > 0 && screenSize !== ScreenSize.SM && (
<div className='p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto shadow-small rounded-large w-full'> <div className='overflow-x-auto'>
<table className='min-w-full h-auto table-auto w-full'> <table className='table min-w-full h-auto table-auto w-full'>
<thead className='[&>tr]:first:rounded-lg'> <thead className='bg-base-200'>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
<tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}> <tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header, headerIndex) => {
return ( return (
<th <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`} className={`${header.column.columnDef.meta?.className ? header.column.columnDef.meta?.className : ''} group/th px-3 h-10 align-middle whitespace-nowrap text-foreground-500 text-tiny font-semibold ${headerGroupIndex === 0 ? 'first:rounded-tl-lg last:rounded-tr-lg' : ''} ${headerGroupIndex === table.getHeaderGroups().length - 1 ? 'first:rounded-bl-lg last:rounded-br-lg' : ''} data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
colSpan={header.colSpan} colSpan={header.colSpan}
key={header.id} key={header.id}
> >
{header.isPlaceholder ? null : ( {header.isPlaceholder ? null : (
<div> <div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
{flexRender( {flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext()
@@ -566,13 +564,15 @@ const Logbook: React.FC<unknown> = () => {
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
return ( return (
<td <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`} className={`py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:z-1 [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
key={cell.id} key={cell.id}
> >
{flexRender( <div className={`${cell.column.columnDef.meta?.align ? cell.column.columnDef.meta?.align : ''}`}>
cell.column.columnDef.cell, {flexRender(
cell.getContext() cell.column.columnDef.cell,
)} cell.getContext()
)}
</div>
</td> </td>
); );
})} })}
@@ -581,7 +581,7 @@ const Logbook: React.FC<unknown> = () => {
})} })}
</> </>
</tbody> </tbody>
<thead className='[&>tr]:first:rounded-lg'> <thead className='[&>tr]:first:rounded-lg bg-base-200'>
{table.getFooterGroups().map((footerGroup, index) => { {table.getFooterGroups().map((footerGroup, index) => {
if (index === 0) { if (index === 0) {
return ( return (
@@ -591,14 +591,16 @@ const Logbook: React.FC<unknown> = () => {
<td <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' className='group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-s-lg last:rounded-e-lg data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start'
key={header.id} key={header.id}
align={header.column.columnDef.meta?.align}
> >
{header.isPlaceholder <div className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''}`}>
? null {header.isPlaceholder
: flexRender( ? null
header.column.columnDef.footer, : flexRender(
header.getContext() header.column.columnDef.footer,
)} header.getContext()
)
}
</div>
</td> </td>
); );
})} })}
@@ -641,14 +643,6 @@ const Logbook: React.FC<unknown> = () => {
title="Confirm Delete" title="Confirm Delete"
/> />
)} )}
{/* {state.isTracksOpen &&
<LogTracks
isDrawerOpen={state.isTracksOpen}
mode={state.tracksMode}
onOpenClose={(mode) => onOpenCloseTracks(mode)}
selectedLogId={logbookContext.state.selectedLogId}
/>
} */}
</> </>
); );
}; };

View File

@@ -48,7 +48,8 @@ const route: ColumnDef<LogbookEntry> = {
id: 'route', id: 'route',
header: 'Route of Flight', header: 'Route of Flight',
meta: { meta: {
headerAlign: 'center' headerAlign: 'text-center',
className: 'border-l border-base-400'
}, },
columns: [ columns: [
{ {
@@ -68,8 +69,8 @@ const durationOfFlight: ColumnDef<LogbookEntry> = {
accessorKey: 'durationOfFlight', accessorKey: 'durationOfFlight',
header: 'Duration Of Flight', header: 'Duration Of Flight',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '', info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
@@ -106,8 +107,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
accessorKey: 'singleEngineLand', accessorKey: 'singleEngineLand',
header: 'Single Engine Land', header: 'Single Engine Land',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '', info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
@@ -117,7 +118,7 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
id: 'landings', id: 'landings',
header: 'Landings', header: 'Landings',
meta: { meta: {
headerAlign: 'center' headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
@@ -126,8 +127,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Day', header: 'Day',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
}, },
{ {
@@ -136,8 +137,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Night', header: 'Night',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
} }
] ]
@@ -146,7 +147,7 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
id: 'instrument', id: 'instrument',
header: 'Instrument', header: 'Instrument',
meta: { meta: {
headerAlign: 'center' headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
@@ -155,8 +156,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Actual', header: 'Actual',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '', info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '',
@@ -167,8 +168,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Simulated', header: 'Simulated',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -179,8 +180,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Approaches', header: 'Approaches',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
}, },
{ {
@@ -189,8 +190,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Holds', header: 'Holds',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
}, },
{ {
@@ -199,8 +200,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Nav/Track', header: 'Nav/Track',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
} }
} }
] ]
@@ -209,7 +210,7 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
id: 'experienceTraining', id: 'experienceTraining',
header: 'Type of pilot experience or training', header: 'Type of pilot experience or training',
meta: { meta: {
headerAlign: 'center' headerAlign: 'text-center'
}, },
columns: [ columns: [
{ {
@@ -218,8 +219,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Ground Training Received', header: 'Ground Training Received',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -230,8 +231,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Flight Training Received', header: 'Flight Training Received',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -242,8 +243,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Cross Country', header: 'Cross Country',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -254,8 +255,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Night', header: 'Night',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -266,8 +267,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Solo', header: 'Solo',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''
@@ -278,8 +279,8 @@ export const authColumns: ColumnDef<LogbookEntry>[] = [
header: 'Pilot In Command', header: 'Pilot In Command',
footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '', footer: (info: HeaderContext<LogbookEntry, unknown>) => columnTotal(info) > 0 ? columnTotal(info).toFixed(1) : '',
meta: { meta: {
align: 'right', align: 'text-right',
headerAlign: 'right' headerAlign: 'text-right'
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => cell: (info: CellContext<LogbookEntry, unknown>) =>
info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : '' info.getValue() ? parseFloat(info.getValue() as string).toFixed(1) : ''

View File

@@ -1,4 +1,3 @@
import { Accordion, AccordionItem, Card, CardBody, CardHeader } from '@heroui/react'
import { LogbookCardProps } from "./LogbookCardProps.interface"; import { LogbookCardProps } from "./LogbookCardProps.interface";
import TrackMap from "../trackMap/TrackMap"; import TrackMap from "../trackMap/TrackMap";
@@ -10,12 +9,10 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
return ( return (
<div> <div className='card bg-base-100 border border-base-300 p-2'>
<Card className='p-4' key={log.id}> <div className='card-body' key={log.id}>
<CardHeader> <h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
<h2 className='font-bold text-2xl'>{formattedDate}</h2> <div>
</CardHeader>
<CardBody>
{mode === 'flights' && log.tracks && log.tracks.length > 0 && {mode === 'flights' && log.tracks && log.tracks.length > 0 &&
<div className='mb-5'> <div className='mb-5'>
<TrackMap <TrackMap
@@ -25,49 +22,50 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
/> />
</div> </div>
} }
<Accordion variant='bordered'> </div>
<AccordionItem key='1' title='Details'> <div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'> <input type='radio' name='details-accordion' />
<div className='col-span-6 font-bold'> <div className='collapse-title font-semibold'>Details</div>
<span>Aircraft Make and Model</span> <div className='collapse-content'>
</div> <div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
<div className='col-span-6'> <div className='col-span-6 font-bold'>
<span>{log.aircraftMakeModel}</span> <span>Aircraft Make and Model</span>
</div>
<div className='col-span-6 font-bold'>
<span>Route From</span>
</div>
<div className='col-span-6'>
<span>{log.routeFrom}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Route To</span>
</div>
<div className='col-span-6'>
<span>{log.routeTo}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Duration Of Flight</span>
</div>
<div className='col-span-6'>
<span>{log.durationOfFlight}</span>
</div>
{log.notes &&
<>
<div className='col-span-6 font-bold'>
<span>Notes</span>
</div>
<div className='col-span-6'>
<span>{log.notes}</span>
</div>
</>
}
</div> </div>
</AccordionItem> <div className='col-span-6'>
</Accordion> <span>{log.aircraftMakeModel}</span>
</div>
</CardBody> <div className='col-span-6 font-bold'>
</Card> <span>Route From</span>
</div>
<div className='col-span-6'>
<span>{log.routeFrom}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Route To</span>
</div>
<div className='col-span-6'>
<span>{log.routeTo}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Duration Of Flight</span>
</div>
<div className='col-span-6'>
<span>{log.durationOfFlight}</span>
</div>
{log.notes &&
<>
<div className='col-span-6 font-bold'>
<span>Notes</span>
</div>
<div className='col-span-6'>
<span>{log.notes}</span>
</div>
</>
}
</div>
</div>
</div>
</div>
</div> </div>
) )
})} })}

View File

@@ -1,7 +1,7 @@
import { Alert, Button, Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Tab, Tabs } from "@heroui/react";
import { LogbookDrawerProps } from "./LogbookDrawerProps.interface"; import { LogbookDrawerProps } from "./LogbookDrawerProps.interface";
import LogForm from "../logForm/LogForm"; import LogForm from "../logForm/LogForm";
import TracksForm from "../tracksForm/TracksForm"; import TracksForm from "../tracksForm/TracksForm";
import Alert from '../alert/Alert';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClock, faMapLocationDot, faSave, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faClock, faMapLocationDot, faSave, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
@@ -9,10 +9,10 @@ import { FormMode } from "../../enums/formMode";
import httpClient from "../../httpClient/httpClient"; import httpClient from "../../httpClient/httpClient";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext"; import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
import { Key, useState } from "react"; import { useState } from "react";
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => { const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
const [activeTab, setActiveTab] = useState<Key>('time'); const [activeTab, setActiveTab] = useState<string>('time');
const defaultValues = { const defaultValues = {
pilotId: '', pilotId: '',
date: null, date: null,
@@ -63,111 +63,107 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }}); logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }});
} finally { } finally {
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false }); logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false });
} }
}; };
const onSelectedKeyChanged = (key: React.Key) => { const onSelectedKeyChanged = (tab: string) => {
setActiveTab(key) setActiveTab(tab)
} }
return ( return (
<Drawer <div className='drawer drawer-end'
closeButton={ // closeButton={
<Button isIconOnly> // <Button isIconOnly>
<FontAwesomeIcon icon={faXmark} /> // <FontAwesomeIcon icon={faXmark} />
</Button> // </Button>
} // }
isOpen={logbookContext.state.isDrawerOpen} // isOpen={logbookContext.state.isDrawerOpen}
onClose={onCancel} // onClose={onCancel}
> >
<DrawerContent> <input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={logbookContext.state.isDrawerOpen} />
<FormProvider {...methods}> <div className="drawer-side">
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}> <label
<DrawerHeader> htmlFor='my-drawer-1'
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`} aria-label='close-sidebar'
</DrawerHeader> className='drawer-overlay'
<DrawerBody> ></label>
{logbookContext.state.formAlert && ( <div className='menu bg-base-100 text-base-content min-h-full p-4' style={{ width: '25%' }}>
<div className='col-span-12'> <FormProvider {...methods}>
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
<h2>
{`${logbookContext.state.formMode.toString().toLowerCase().charAt(0).toUpperCase() + logbookContext.state.formMode.toString().slice(1).toLowerCase()} Entry`}
</h2>
{logbookContext.state.formAlert && (
<Alert <Alert
className='mb-5'
onClose={() => onClose={() =>
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined }) logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: undefined })
} }
color={logbookContext.state.formAlert.severity} severity={logbookContext.state.formAlert.severity}
title={logbookContext.state.formAlert.message}
/>
</div>
)}
<Tabs
color='default'
fullWidth={true}
onSelectionChange={onSelectedKeyChanged}
selectedKey={activeTab as string}
variant='solid'
>
<Tab
key='time'
title={
<div className="flex items-center space-x-2">
<FontAwesomeIcon icon={faClock} />
<span>Time</span>
</div>
}
>
<LogForm />
</Tab>
{logbookContext.state.formMode !== FormMode.ADD &&
<Tab
key='tracks'
title={
<div className="flex items-center space-x-2">
<FontAwesomeIcon icon={faMapLocationDot} />
<span>Tracks</span>
</div>
}
> >
<TracksForm /> {logbookContext.state.formAlert.message}
</Tab> </Alert>
} )}
</Tabs> <div className='tabs tabs-lift mb-5'>
</DrawerBody> <label className='tab'>
{activeTab !== 'tracks' && <input type='radio' name='my_tabs' defaultChecked={true} />
<DrawerFooter> <FontAwesomeIcon className='mr-1'icon={faClock} />
<div className='grid grid-cols-12 gap-3'> Time
<div className='col-span-12 justify-self-end self-center'> </label>
<Button <div className='tab-content border-base-300 p-6'>
disabled={ <LogForm />
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
? logbookContext.state.isFormDisabled
: false
}
startContent={<FontAwesomeIcon icon={faXmark} />}
onPress={onCancel}
>
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</Button>
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
<Button
className='ml-[10px]'
color='primary'
disabled={logbookContext.state.isFormDisabled}
startContent={<FontAwesomeIcon icon={faSave} />}
type="submit"
>
Save
</Button>
)}
</div> </div>
{logbookContext.state.formMode !== FormMode.ADD &&
<>
<label className='tab'>
<input type='radio' name='my_tabs'/>
<FontAwesomeIcon className='mr-1' icon={faMapLocationDot} />
Tracks
</label>
<div className='tab-content border-base-300 p-6'>
<TracksForm />
</div>
</>
}
</div> </div>
</DrawerFooter> {activeTab !== 'tracks' &&
} <div>
</form> <div className='grid grid-cols-12 gap-3'>
</FormProvider> <div className='col-span-12 justify-self-end self-center'>
</DrawerContent> <button
</Drawer> className='btn'
disabled={
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
? logbookContext.state.isFormDisabled
: false
}
onClick={onCancel}
>
<FontAwesomeIcon icon={faXmark} />
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</button>
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
<button
className='btn btn-primary ml-2.5'
disabled={logbookContext.state.isFormDisabled}
type="submit"
>
<FontAwesomeIcon icon={faSave} />
Save
</button>
)}
</div>
</div>
</div>
}
</form>
</FormProvider>
</div>
</div>
</div>
) )
} }

View File

@@ -1,4 +1,4 @@
import { Key, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm, Controller, FormProvider } from 'react-hook-form'; import { useForm, Controller, FormProvider } from 'react-hook-form';
import { IPilotFormProps } from './IPilotFormProps'; import { IPilotFormProps } from './IPilotFormProps';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
@@ -9,7 +9,6 @@ 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 httpClient from '../../httpClient/httpClient'; import httpClient from '../../httpClient/httpClient';
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input, Autocomplete, AutocompleteItem, SelectItem, Select } from '@heroui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
import { states } from './states'; import { states } from './states';
@@ -45,39 +44,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 onPeoplePickerSearch = async (
value: string
) => {
setIsPeoplePickerLoading(true);
setPeoplePickerValue(value)
try {
if (value !== '') {
const searchString: string = value;
const response: AxiosResponse = await httpClient.get(
`api/msgraph/search?search=${searchString}`
);
setPeoplePickerResults(response.data);
} else {
setPeoplePickerResults([]);
}
} catch (error) {
console.log(error);
} finally {
setIsPeoplePickerLoading(false);
}
};
const onPersonSelected = (userPrincipalName: string) => {
const person: Person | undefined = peoplePickerResults.find((person) => person.userPrincipalName === userPrincipalName as string);
methods.setValue('name', person?.displayName!);
methods.setValue('userId', person?.userPrincipalName!);
setPeoplePickerValue(person?.displayName!);
setPeoplePickerResults([])
};
const onCancel = () => { const onCancel = () => {
methods.reset(defaultValues); methods.reset(defaultValues);
onOpenClose(FormMode.CANCEL); onOpenClose(FormMode.CANCEL);
@@ -138,245 +104,229 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}, [pilotId]); }, [pilotId]);
return ( return (
<Drawer <div className='drawer drawer-end'
closeButton={ // closeButton={
<Button isIconOnly> // <Button isIconOnly>
<FontAwesomeIcon icon={faXmark} /> // <FontAwesomeIcon icon={faXmark} />
</Button> // </Button>
} // }
isOpen={isDrawerOpen} // isOpen={isDrawerOpen}
placement='right' // placement='right'
data-testid="pilot-drawer" // data-testid="pilot-drawer"
onClose={onCancel} // onClose={onCancel}
size='xl' // size='xl'
> >
<DrawerContent> <input type='checkbox' className='drawer-toggle' onChange={() => {}} checked={isDrawerOpen} />
<FormProvider {...methods}> <div className='drawer-side'>
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}> <label
<DrawerHeader> htmlFor='my-drawer-1'
{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`} aria-label='close-sidebar'
</DrawerHeader> className='drawer-overlay'
<DrawerBody> ></label>
<div className='grid grid-cols-12 gap-3'> <div className='menu bg-base-100 text-base-content min-h-full p-4' style={{ width: '25%' }}>
<div className='col-span-3 self-center'> <FormProvider {...methods}>
<h6>Name *</h6> <form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
</div> <h2>
<div className='col-span-9'> {`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Pilot`}
<Autocomplete </h2>
inputValue={peoplePickerValue} <div className='grid grid-cols-12 gap-3'>
isLoading={isPeoplePickerLoading} <div className='col-span-3 self-center'>
items={peoplePickerResults} <span>Name *</span>
onInputChange={onPeoplePickerSearch} </div>
onSelectionChange={(key: Key | null) => onPersonSelected(key as string)} <div className='col-span-9'>
> <Controller
{peoplePickerResults.map((person: Person) => ( name="name"
<AutocompleteItem key={person.userPrincipalName}> control={methods.control}
{person.displayName} rules={{ required: 'A name is required' }}
</AutocompleteItem> render={({ field: { onChange, value } }) => (
))} <input
</Autocomplete> type='text'
</div> className='input w-full'
{isUserLoggedIn && disabled={isDisabled}
<> onChange={onChange}
<div className='col-span-3 self-center'> value={value}
<span>Address *</span> />
</div> )}
<div className='col-span-9'>
<Controller
name="address"
control={methods.control}
rules={{ required: 'An address is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
color={methods.formState.errors.address ? 'danger' : undefined}
errorMessage={
methods.formState.errors.address
? methods.formState.errors.address.message
: undefined
}
fullWidth={true}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>City *</h6>
</div>
<div className='col-span-9'>
<Controller
name="city"
control={methods.control}
rules={{ required: 'A city is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
color={methods.formState.errors.city ? 'danger' : undefined}
errorMessage={
methods.formState.errors.city
? methods.formState.errors.city.message
: undefined
}
fullWidth={true}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>State *</h6>
</div>
<div className='col-span-9'>
<Controller
name="state"
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { onChange, value } }) => (
<Select
onChange={onChange}
selectedKeys={[value]}
>
{states.map((state) => (
<SelectItem key={state.value}>{state.label}</SelectItem>
))}
</Select>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>Postal Code *</h6>
</div>
<div className='col-span-9'>
<Controller
name="postalCode"
control={methods.control}
rules={{ required: 'A postal code is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
color={methods.formState.errors.postalCode ? 'danger' : undefined}
errorMessage={
methods.formState.errors.postalCode
? methods.formState.errors.postalCode.message
: undefined
}
fullWidth={true}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>Email</h6>
</div>
<div className='col-span-9'>
<Controller
name="email"
control={methods.control}
rules={{
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
}}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
color={methods.formState.errors.email ? 'danger' : undefined}
errorMessage={
methods.formState.errors.email
? methods.formState.errors.email.message
: undefined
}
fullWidth={true}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>Phone Number</h6>
</div>
<div className='col-span-9'>
<Controller
name="phone"
control={methods.control}
rules={{
pattern: {
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
message: 'Enter phone number as 123-456-7890'
}
}}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
color={methods.formState.errors.phone ? 'danger' : undefined}
errorMessage={
methods.formState.errors.phone
? methods.formState.errors.phone.message
: undefined
}
fullWidth={true}
onChange={onChange}
value={value}
/>
)}
/>
</div>
</>
}
{/* {isAuthenticated &&
<Grid size={12}>
<PilotFormMedical
isDisabled={isDisabled}
/> />
</div>
{isUserLoggedIn &&
<>
<div className='col-span-3 self-center'>
<span>Address *</span>
</div>
<div className='col-span-9'>
<Controller
name="address"
control={methods.control}
rules={{ required: 'An address is required' }}
render={({ field: { onChange, value } }) => (
<input
type='text'
className='input w-full'
disabled={isDisabled}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>City *</h6>
</div>
<div className='col-span-9'>
<Controller
name="city"
control={methods.control}
rules={{ required: 'A city is required' }}
render={({ field: { onChange, value } }) => (
<input
type='text'
className='input w-full'
disabled={isDisabled}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>State *</h6>
</div>
<div className='col-span-9'>
<Controller
name="state"
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { onChange, value } }) => (
<select
className='select w-full'
onChange={onChange}
value={[value]}
>
{states.map((state) => (
<option key={state.value}>{state.label}</option>
))}
</select>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>Postal Code *</h6>
</div>
<div className='col-span-9'>
<Controller
name="postalCode"
control={methods.control}
rules={{ required: 'A postal code is required' }}
render={({ field: { onChange, value } }) => (
<input
type='text'
className='input w-full'
disabled={isDisabled}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>Email</h6>
</div>
<div className='col-span-9'>
<Controller
name="email"
control={methods.control}
rules={{
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
}}
render={({ field: { onChange, value } }) => (
<input
type='text'
className='input w-full'
disabled={isDisabled}
onChange={onChange}
value={value}
/>
)}
/>
</div>
<div className='col-span-3 self-center'>
<h6>Phone Number</h6>
</div>
<div className='col-span-9'>
<Controller
name="phone"
control={methods.control}
rules={{
pattern: {
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
message: 'Enter phone number as 123-456-7890'
}
}}
render={({ field: { onChange, value } }) => (
<input
type='text'
className='input w-full'
disabled={isDisabled}
onChange={onChange}
value={value}
/>
)}
/>
</div>
</>
}
{/* {isAuthenticated &&
<Grid size={12}>
<PilotFormMedical
isDisabled={isDisabled}
/>
</Grid>
}
<Grid size={12}>
<PilotFormCertificates isDisabled={isDisabled} mode={mode} />
</Grid> </Grid>
} <Grid size={12}>
<Grid size={12}> <PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
<PilotFormCertificates isDisabled={isDisabled} mode={mode} /> </Grid> */}
</Grid> {/* <div className='col-span-12 justify-self-end self-center'> */}
<Grid size={12}>
<PilotFormEndorsements isDisabled={isDisabled} mode={mode} />
</Grid> */}
{/* <div className='col-span-12 justify-self-end self-center'> */}
{/* </div> */}
</div> <div className='col-span-12 justify-self-end self-center'>
</DrawerBody> <button
<DrawerFooter> className='btn'
<Button disabled={
disabled={ isDisabled && mode.toString() !== FormMode.VIEW
isDisabled && mode.toString() !== FormMode.VIEW ? isDisabled
? isDisabled : false
: false }
} onClick={onCancel}
startContent={<FontAwesomeIcon icon={faXmark} />} data-testid="pilot-cancel-button"
onPress={onCancel}
data-testid="pilot-cancel-button"
>
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</Button>
{mode.toString() !== FormMode.VIEW && (
<Button
color='primary'
disabled={isDisabled}
startContent={<FontAwesomeIcon icon={faSave} />}
type="submit"
data-testid="pilot-save-button"
> >
Save <FontAwesomeIcon icon={faXmark} />
</Button> {mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
)} </button>
</DrawerFooter> {mode.toString() !== FormMode.VIEW && (
</form> <button
</FormProvider> className='btn btn-primary ml-2.5'
</DrawerContent> disabled={isDisabled}
</Drawer> type="submit"
data-testid="pilot-save-button"
>
<FontAwesomeIcon icon={faSave} />
Save
</button>
)}
</div>
</div>
</form>
</FormProvider>
</div>
</div>
</div>
); );
}; };

View File

@@ -10,14 +10,18 @@ import { UserRole } from '../../enums/userRole';
import httpClient from '../../httpClient/httpClient' import httpClient from '../../httpClient/httpClient'
import { ScreenSize } from '../../enums/screenSize'; import { ScreenSize } from '../../enums/screenSize';
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints'; import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEllipsisVertical, faPen, faEye, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons' import { faEllipsisVertical, faPen, faEye, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons';
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable } from '@tanstack/react-table';
import { Pilot } from './Pilot.interface';
import Alert from '../alert/Alert';
import { useOidc } from '../../auth/oidcConfig';
const Pilots: React.FC<unknown> = () => { const Pilots: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const { userRole } = useUserRole(); const { userRole } = useUserRole();
const { screenSize } = useBreakpoints() const { screenSize } = useBreakpoints();
const { isUserLoggedIn } = useOidc();
const getPilots = async () => { const getPilots = async () => {
try { try {
@@ -27,6 +31,8 @@ const Pilots: React.FC<unknown> = () => {
`api/pilots` `api/pilots`
); );
console.log(response)
if (response.data.length > 0) { if (response.data.length > 0) {
dispatch({ type: 'SET_PILOTS', payload: response.data }); dispatch({ type: 'SET_PILOTS', payload: response.data });
@@ -34,7 +40,7 @@ const Pilots: React.FC<unknown> = () => {
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_ALERT', payload: undefined })
} }
} else { } else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }}) dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No pilots found.' }})
dispatch({ type: 'SET_PILOTS', payload: [] }); dispatch({ type: 'SET_PILOTS', payload: [] });
} }
} catch (error) { } catch (error) {
@@ -42,7 +48,7 @@ const Pilots: React.FC<unknown> = () => {
dispatch({ dispatch({
type: 'SET_ALERT', type: 'SET_ALERT',
payload: { severity: 'danger', message: `Loading of pilots failed with the following message: ${axiosError.message}`} payload: { severity: 'error', message: `Loading of pilots failed with the following message: ${axiosError.message}`}
}) })
} finally { } finally {
dispatch({ type: 'SET_IS_LOADING', payload: false }) dispatch({ type: 'SET_IS_LOADING', payload: false })
@@ -101,7 +107,7 @@ const Pilots: React.FC<unknown> = () => {
dispatch({ dispatch({
type: 'SET_ALERT', type: 'SET_ALERT',
payload: { severity: 'danger', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`} payload: { severity: 'error', message: `Loading of logbook entries failed with the following message: ${axiosError.message}`}
}); });
} finally { } finally {
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false }); dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: false });
@@ -115,64 +121,50 @@ const Pilots: React.FC<unknown> = () => {
}); });
}; };
const columns = [ const columns: ColumnDef<Pilot>[]= [
{ {
id: 'name', id: 'name',
name: 'Name' accessorKey: 'name',
header: 'Name'
}, },
{ {
id: 'actions', id: 'actions',
name: 'Actions' header: 'Actions',
meta: {
align: 'text-center',
headerAlign: 'text-center'
},
cell: (info: CellContext<Pilot, unknown>) => {
return (
<div className='dropdown dropdown-end'>
<div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300">
{isUserLoggedIn && userRole === UserRole.WRITE &&
<li><a onClick={() => onOpenClosePilotForm(FormMode.EDIT, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
}
<li><a onClick={() => onOpenClosePilotForm(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
{isUserLoggedIn && userRole === UserRole.WRITE &&
<li><a onClick={() => onDeletePilot(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
}
</ul>
</div>
)
}
} }
] ]
const renderCell = (pilot: any, columnKey: any) => { const textAlignment = {
const cellValue = pilot[columnKey] center: 'text-center',
left: 'text-start',
right: 'text-end'
};
switch (columnKey) { const table = useReactTable({
case 'actions': { data: state.pilots,
return ( columns: columns,
<Dropdown> getCoreRowModel: getCoreRowModel(),
<DropdownTrigger> getPaginationRowModel: getPaginationRowModel()
<Button isIconOnly variant='light' size='lg'> });
<FontAwesomeIcon icon={faEllipsisVertical} />
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownSection showDivider>
<DropdownItem
key='edit'
onPress={() => onOpenClosePilotForm(FormMode.EDIT, pilot.id)}
startContent={<FontAwesomeIcon icon={faPen} />}
>
Edit
</DropdownItem>
<DropdownItem
key='view'
onPress={() => onOpenClosePilotForm(FormMode.VIEW, pilot.id)}
startContent={<FontAwesomeIcon icon={faEye} />}
>
View
</DropdownItem>
</DropdownSection>
<DropdownSection>
<DropdownItem
key='Delete'
onPress={() => onDeletePilot(pilot.id)}
startContent={<FontAwesomeIcon icon={faTrash} />}
>
Delete
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
)
}
default: {
return cellValue
}
}
}
useEffect(() => { useEffect(() => {
if (!state.isFormOpen) { if (!state.isFormOpen) {
@@ -188,52 +180,85 @@ const Pilots: React.FC<unknown> = () => {
</div> </div>
<div className='col-span-2 justify-self-end self-center'> <div className='col-span-2 justify-self-end self-center'>
{userRole === UserRole.WRITE && {userRole === UserRole.WRITE &&
<Button <button
color='primary' className='btn btn-primary'
onPress={() => onOpenClosePilotForm(FormMode.ADD)} onClick={() => onOpenClosePilotForm(FormMode.ADD)}
startContent={<FontAwesomeIcon icon={faPlus} />}
data-testid="pilot-add-button" data-testid="pilot-add-button"
> >
<FontAwesomeIcon icon={faPlus} />
Add Pilot Add Pilot
</Button> </button>
} }
</div> </div>
{!state.isLoading && state.alert && ( {!state.isLoading && state.alert && (
<div className='col-span-12'> <div className='col-span-12'>
<Alert <Alert
className='mb-5'
onClose={() => onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_ALERT', payload: undefined })
} }
color={state.alert.severity} severity={state.alert.severity}
title={state.alert.message} >
/> {state.alert.message}
</Alert>
</div> </div>
)} )}
<div className='col-span-12'> <div className='col-span-12 bg-base-100 p-5 border border-base-100 rounded-lg'>
{state.pilots.length > 0 && screenSize !== ScreenSize.SM && {state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
<Table> <table className='table min-w-full h-auto table-auto w-full'>
<TableHeader columns={columns}> <thead className='[&>tr]:first:rounded-lg bg-base-200'>
{(column) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableColumn <tr className='group/tr outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2' key={headerGroup.id}>
key={column.id} {headerGroup.headers.map((header) => {
align={column.id === "actions" ? "center" : "start"} return (
> <th
{column.name} className={`${header.column.columnDef.meta?.headerAlign ? header.column.columnDef.meta?.headerAlign : ''} group/th px-3 h-10 align-middle bg-default-100 whitespace-nowrap text-foreground-500 text-tiny font-semibold rounded data-[sortable=true]:cursor-pointer data-[hover=true]:text-foreground-400 outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-start`}
</TableColumn> colSpan={header.colSpan}
)} key={header.id}
</TableHeader> >
<TableBody items={state.pilots}> {header.isPlaceholder ? null : (
{(item) => ( <div>
<TableRow key={item.id}> {flexRender(
{(columnKey => ( header.column.columnDef.header,
<TableCell> header.getContext()
{renderCell(item, columnKey)} )}
</TableCell> {/* {header.column.getCanFilter() ? (
))} <div>
</TableRow> <Filter column={header.column} table={table} />
)} </div>
</TableBody> ) : null} */}
</Table> </div>
)}
</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 ? cell.column.columnDef.meta?.align : ''} py-2 px-3 relative align-middle whitespace-normal text-small font-normal [&>*]:z-1 [&>*]:relative outline-solid outline-transparent data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 before:pointer-events-none before:content-[''] before:absolute before:z-0 before:inset-0 before:opacity-0 data-[selected=true]:before:opacity-100 group-data-[disabled=true]/tr:text-foreground-300 group-data-[disabled=true]/tr:cursor-not-allowed before:bg-default/60 data-[selected=true]:text-default-foreground first:before:rounded-s-lg last:before:rounded-e-lg text-start`}
key={cell.id}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
);
})}
</tr>
);
})}
</>
</tbody>
</table>
} }
{state.pilots.length > 0 && screenSize === ScreenSize.SM && {state.pilots.length > 0 && screenSize === ScreenSize.SM &&
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} /> <PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />

View File

@@ -3,17 +3,18 @@ import { useAppContext } from '../../hooks/appContext/UseAppContext';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { User } from '@microsoft/microsoft-graph-types'; import { User } from '@microsoft/microsoft-graph-types';
import { useOidc } from '../../auth/oidcConfig'; import { useOidc } from '../../auth/oidcConfig';
import { Avatar, Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, DropdownTrigger, DropdownMenu, DropdownItem, Dropdown } from '@heroui/react';
import httpClient from '../../httpClient/httpClient' import httpClient from '../../httpClient/httpClient'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons' import { faBars, faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
import { useLocation } from 'react-router-dom'; import { NavLink, useLocation } from 'react-router-dom';
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
import { ScreenSize } from '../../enums/screenSize';
const SiteNav = () => { const SiteNav = () => {
const [userPhoto, setUserPhoto] = useState<string>(); const [userPhoto, setUserPhoto] = useState<string>();
const appContext = useAppContext(); const appContext = useAppContext();
const { screenSize } = useBreakpoints();
const { isUserLoggedIn, logout, login } = useOidc() const { isUserLoggedIn, logout, login } = useOidc()
const { pathname } = useLocation()
const pages = [ const pages = [
{ {
name: 'Flights', name: 'Flights',
@@ -53,13 +54,39 @@ const SiteNav = () => {
} }
}; };
const Brand = () => {
return (
<>
<img
className='mr-1'
height={35}
width={35}
src='noahspan-logo.png'
/>
<FontAwesomeIcon className='mt-1' icon={faPlane} size='2x' />
</>
)
}
const Links = () => {
return (
<>
{pages.map((page) => {
return (
<li><NavLink to={page.path}>{page.name}</NavLink></li>
)
})}
</>
)
}
useEffect(() => { useEffect(() => {
const setUserProfile = async () => { const setUserProfile = async () => {
try { try {
const userProfile = await getUserProfile(); const userProfile = await getUserProfile();
const userPhoto = await getUserPhoto(); // const userPhoto = await getUserPhoto();
setUserPhoto(userPhoto); // setUserPhoto(userPhoto);
appContext.dispatch({ appContext.dispatch({
type: 'SET_USER_PROFILE', type: 'SET_USER_PROFILE',
@@ -78,54 +105,67 @@ const SiteNav = () => {
} }
}, [isUserLoggedIn]); }, [isUserLoggedIn]);
useEffect(() => {
console.log(screenSize)
}, [screenSize])
return ( return (
<Navbar isBordered maxWidth='full' position='static'> <div className="navbar bg-base-100 shadow-sm w-full">
<NavbarContent> <div className={`navbar-start ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
<NavbarBrand> {screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
<img <div className="dropdown">
height={35} <div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
width={35} <FontAwesomeIcon icon={faBars} size='xl' />
src='noahspan-logo.png' </div>
style={{ marginRight: '5px' }} <ul
/> tabIndex={-1}
<FontAwesomeIcon icon={faPlane} size='2x' /> className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
</NavbarBrand> <Links />
</NavbarContent> </ul>
<NavbarContent justify='center'> </div>
{pages.length > 0 && pages.map((page, index) => { ) : (
return ( <Brand />
<NavbarItem isActive={pathname === page.path ? true : false} key={index}> )}
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}> </div>
{page.name} <div className="navbar-center">
</Link> <ul className="menu menu-horizontal px-1">
</NavbarItem> {screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? (
) <Brand />
})} ) : (
</NavbarContent> <Links />
<NavbarContent justify='end'> )}
</ul>
</div>
<div className={`navbar-end ${screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD ? 'ml-8' : ''}`}>
{!isUserLoggedIn && {!isUserLoggedIn &&
<Button <button className='btn btn-ghost' onClick={() => login()}><FontAwesomeIcon icon={faSignIn} />Sign In</button>
color='default'
onPress={() => login()}
startContent={<FontAwesomeIcon icon={faSignIn} />}
>
Sign In
</Button>
} }
{isUserLoggedIn && {isUserLoggedIn &&
<Dropdown> <div className='dropdown dropdown-end'>
<DropdownTrigger> <div tabIndex={0} role='button'>
<Avatar name={appContext.state.userProfile.displayName?.toString()} src={userPhoto}></Avatar> <div className={`avatar ${userPhoto ? userPhoto : 'avatar-placeholder'}`}>
</DropdownTrigger> {userPhoto &&
<DropdownMenu> <div className='w-12 rounded-full'>
<DropdownItem key='signout' onPress={() => logout({redirectTo: 'specific url', url: '/'})} startContent={<FontAwesomeIcon icon={faSignOut} />}> <img src={userPhoto} />
Sign Out </div>
</DropdownItem> }
</DropdownMenu> {!userPhoto &&
</Dropdown> <div className='bg-neutral text-neutral-content w-10 rounded-full'>
<span>NS</span>
</div>
}
</div>
</div>
<ul
tabIndex={0}
className='dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300'
>
<li><a onClick={() => logout({redirectTo: 'specific url', url: '/'})}><FontAwesomeIcon icon={faSignOut} />Sign Out</a></li>
</ul>
</div>
} }
</NavbarContent> </div>
</Navbar> </div>
); );
}; };

View File

@@ -1,53 +0,0 @@
#app {
height: 100%;
}
html,
body {
position: relative;
height: 100%;
}
body {
background: #eee;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #000;
margin: 0;
padding: 0;
}
.swiper {
width: 100%;
height: 100%;
}
.swiper-wrapper {
margin-bottom: 20px;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.swiper-slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.swiper-pagination-bullet {
background-color: #000000;
height: 13px;
width: 13px;
border: 2px solid #FFFFFF;
}
.swiper-pagination-bullet-active {
box-shadow: 0 0 0 1px #000000;
}

View File

@@ -4,10 +4,6 @@ import { AxiosInstance, AxiosResponse } from 'axios';
import { useAuth } from 'react-oidc-context' import { useAuth } from 'react-oidc-context'
import { MapContainer, TileLayer } from 'react-leaflet'; import { MapContainer, TileLayer } from 'react-leaflet';
import ReactLeafletKml from 'react-leaflet-kml'; import ReactLeafletKml from 'react-leaflet-kml';
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css';
import './TrackMap.css';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import httpClient from '../../httpClient/httpClient' import httpClient from '../../httpClient/httpClient'
@@ -41,7 +37,7 @@ const TrackMap = ({ height, logId, tracks }: TrackMapProps) => {
center={[45.14489, -93.21019]} center={[45.14489, -93.21019]}
scrollWheelZoom={false} scrollWheelZoom={false}
style={{ height: height, width: '100%' }} style={{ height: height, width: '100%' }}
zoom={8} zoom={7}
> >
<Suspense> <Suspense>
<TileLayer <TileLayer

View File

@@ -7,7 +7,6 @@ import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
import { initialState, reducer } from "./reducer"; import { initialState, reducer } from "./reducer";
import TrackMap from "../trackMap/TrackMap"; import TrackMap from "../trackMap/TrackMap";
import httpClient from "../../httpClient/httpClient"; import httpClient from "../../httpClient/httpClient";
import { Button, Input, Spinner } from '@heroui/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext"; import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
@@ -28,7 +27,7 @@ const TracksForm = () => {
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }}) logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }})
} }
} }
@@ -108,11 +107,6 @@ const TracksForm = () => {
return ( return (
<div className='grid grid-cols-12 gap-3'> <div className='grid grid-cols-12 gap-3'>
{state.tracks.length > 0 &&
<div className="col-span-12">
<TrackMap height='400px' logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
</div>
}
<> <>
{state.tracks.length > 0 && state.tracks.map((track, index) => { {state.tracks.length > 0 && state.tracks.map((track, index) => {
const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1); const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
@@ -120,26 +114,25 @@ const TracksForm = () => {
return ( return (
<> <>
<div className='col-span-10'> <div className='col-span-10'>
<Input isDisabled={state.isDisabled} key={index} type='text' value={filename}/> <input className='input w-full' disabled={state.isDisabled} key={index} readOnly type='text' value={filename} />
</div> </div>
<div className='col-span-2'> <div className='col-span-2'>
<Button isDisabled={state.isDisabled} key={index} isIconOnly onPress={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></Button> <button className='btn w-full' disabled={state.isDisabled} key={index} onClick={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></button>
</div> </div>
</> </>
) )
})} })}
<div className='col-span-12'> {logbookContext.state.formMode === FormMode.ADD &&
<Button <div className='col-span-12'>
as='label' <label
color='primary' className='btn cursor-pointer w-full'
isDisabled={state.isDisabled} >
fullWidth={true} <FontAwesomeIcon icon={faUpload} />
startContent={<FontAwesomeIcon icon={faUpload} />} Upload Track
> <input className='hidden' id='track-upload' onChange={handleFileUpload} type='file' />
Upload Track </label>
<input hidden onChange={handleFileUpload} type='file' /> </div>
</Button> }
</div>
{state.isConfirmDialogOpen && ( {state.isConfirmDialogOpen && (
<ConfirmationDialog <ConfirmationDialog
contentText="Are you sure you want to delete this track?" contentText="Are you sure you want to delete this track?"

View File

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

View File

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

View File

@@ -14,17 +14,17 @@ export const useBreakpoints = () => {
break; break;
} }
case width >= 640: { case width >= 640 && width < 1024: {
size = ScreenSize.MD; size = ScreenSize.MD;
break; break;
} }
case width >= 1024: { case width >= 1024 && width < 1280: {
size = ScreenSize.LG size = ScreenSize.LG
break; break;
} }
case width >= 1280: { case width >= 1280 && width < 1536: {
size = ScreenSize.XL; size = ScreenSize.XL;
break; break;

View File

@@ -15,7 +15,7 @@ httpClient.interceptors.request.use(async (config) => {
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

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flying</title> <title>Flying</title>
</head> </head>
<body> <body style="background-color: #f5f5f5;">
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View File

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

View File

@@ -1,15 +1,42 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin './hero.ts'; @plugin "daisyui";
/* Note: You may need to change the path to fit your project structure */ @plugin "daisyui/theme" {
@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; name: "lofi";
@custom-variant dark (&:is(.dark *)); default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(100% 0 0);
--color-base-200: oklch(97% 0 0);
--color-base-300: oklch(94% 0 0);
--color-base-content: oklch(0% 0 0);
--color-primary: oklch(15.906% 0 0);
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(21.455% 0.001 17.278);
--color-secondary-content: oklch(100% 0 0);
--color-accent: oklch(26.861% 0 0);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(0% 0 0);
--color-neutral-content: oklch(100% 0 0);
--color-info: oklch(79.54% 0.103 205.9);
--color-info-content: oklch(15.908% 0.02 205.9);
--color-success: oklch(90.13% 0.153 164.14);
--color-success-content: oklch(18.026% 0.03 164.14);
--color-warning: oklch(88.37% 0.135 79.94);
--color-warning-content: oklch(17.674% 0.027 79.94);
--color-error: oklch(78.66% 0.15 28.47);
--color-error-content: oklch(15.732% 0.03 28.47);
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 0;
--noise: 0;
}
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@theme inline { body {
--color-primary: #000000; background-color: #f5f5f5;
} min-height: 100vh;
}

View File

@@ -3,8 +3,9 @@ import '@tanstack/react-table';
/* eslint-disable */ /* eslint-disable */
declare module '@tanstack/react-table' { declare module '@tanstack/react-table' {
interface ColumnMeta<TData extends RowData, TValue> { interface ColumnMeta<TData extends RowData, TValue> {
align?: 'left' | 'center' | 'right'; align?: 'text-left' | 'text-center' | 'text-right';
headerAlign?: 'left' | 'center' | 'right'; className?: string;
headerAlign?: 'text-left' | 'text-center' | 'text-right';
} }
} }
/* eslint-enable */ /* eslint-enable */

View File

@@ -5,6 +5,7 @@ interface ImportMetaEnv {
readonly VITE_CLIENT_ID: string; readonly VITE_CLIENT_ID: string;
readonly VITE_ISSUER_URI: string; readonly VITE_ISSUER_URI: string;
readonly VITE_TENANT_ID: string; readonly VITE_TENANT_ID: string;
readonly VITE_USERINFO_ENDPOINT: string;
} }
interface ImportMeta { interface ImportMeta {

Binary file not shown.

View File

@@ -74,6 +74,16 @@ resource "azurerm_container_app" "container_app" {
secret_name = "azure-storage-connection-string" secret_name = "azure-storage-connection-string"
} }
env {
name = "AUTHORITY"
value = var.AUTHORITY
}
env {
name = "AUDIENCE"
value = var.CLIENT_ID
}
env { env {
name = "CLIENT_ID" name = "CLIENT_ID"
value = var.CLIENT_ID value = var.CLIENT_ID
@@ -84,6 +94,16 @@ resource "azurerm_container_app" "container_app" {
secret_name = "client-secret" secret_name = "client-secret"
} }
env {
name = "ISSUER_URL"
value = var.ISSUER_URL
}
env {
name = "JWKS_URI"
value = var.JWKS_URI
}
env { env {
name = "TENANT_ID" name = "TENANT_ID"
value = var.EXTERNAL_TENANT_ID value = var.EXTERNAL_TENANT_ID

View File

@@ -1,3 +1,6 @@
variable "AUTHORITY" {
type = string
}
variable "CLIENT_ID" { variable "CLIENT_ID" {
type = string type = string
@@ -17,6 +20,14 @@ variable "DOCKER_IO_USERNAME" {
type = string type = string
} }
variable "ISSUER_URL" {
type = string
}
variable "JWKS_URI" {
type = string
}
variable "RESOURCE_GROUP_NAME" { variable "RESOURCE_GROUP_NAME" {
type = string type = string
} }

3078
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",
"daisyui": "^5.5.5",
"eslint": "^8.42.0", "eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",