Feature/21 pilots view (#23)

* adding pilot details view

* adding pilot details view

* pilot view

* pilot view
This commit was merged in pull request #23.
This commit is contained in:
2024-10-03 20:43:11 -05:00
committed by GitHub
parent 071e211525
commit 11e00b5b14
15 changed files with 570 additions and 320 deletions

View File

@@ -16,7 +16,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.6.8",
"@noahspan/noahspan-components": "^0.7.0",
"axios": "^1.7.2",
"framer-motion": "^11.1.7",
"react": "^18.2.0",

View File

@@ -1,5 +1,8 @@
import { PilotFormMode } from './PilotForm';
export interface IPilotFormProps {
pilotId?: string;
isDrawerOpen: boolean;
onOpenCloseDrawer: () => void;
mode: PilotFormMode;
onOpenClose: (mode: PilotFormMode) => void;
pilotId?: string;
}

View File

@@ -1,49 +1,60 @@
import { useEffect, useState } from 'react';
import {
useForm,
Controller,
FormProvider,
FieldValues
} from 'react-hook-form';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import {
Button,
DatePicker,
Drawer,
DrawerBody,
DrawerHeader,
DrawerFooter,
Input,
Option,
PeoplePicker,
SaveIcon,
Select,
StateSelect,
Typography,
XmarkIcon
} from '@noahspan/noahspan-components';
import { IPilotFormProps } from './IPilotFormProps';
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
import { Person } from '@microsoft/microsoft-graph-types';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { IPilotFormCertificates } from '../pilotFormCertificates/IPilotFormCertificates';
import { IPilotFormEndorsements } from '../pilotFormEndorsements/IPilotFormEndorsements';
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
import { useIsAuthenticated } from '@azure/msal-react';
export enum PilotFormMode {
ADD = 'ADD',
EDIT = 'EDIT',
VIEW = 'VIEW',
CANCEL = 'CANCEL'
}
const PilotForm: React.FC<IPilotFormProps> = ({
pilotId,
isDrawerOpen,
onOpenCloseDrawer
mode,
onOpenClose
}: IPilotFormProps) => {
const httpClient: AxiosInstance = useHttpClient();
const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]);
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { getAccessToken } = useAccessToken();
const methods = useForm();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
id: '',
name: '',
address: '',
city: '',
state: '',
postalCode: '',
email: '',
phone: ''
};
const methods = useForm({
defaultValues: defaultValues
});
const [isDisabled, setIsDisabled] = useState<boolean>(false);
const handlePeoplePickerOnClick = (
event: React.MouseEvent<HTMLDivElement>
@@ -51,7 +62,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
const divElement: HTMLDivElement = event.target as HTMLDivElement;
methods.setValue('id', divElement.id);
methods.setValue('name', divElement.textContent);
methods.setValue('name', divElement.textContent!);
setPeoplePickerResults([]);
};
@@ -82,6 +93,11 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
};
const onCancel = () => {
methods.reset(defaultValues);
onOpenClose(PilotFormMode.CANCEL);
};
const onSubmit = async (data: unknown) => {
try {
setIsLoading(true);
@@ -111,8 +127,38 @@ const PilotForm: React.FC<IPilotFormProps> = ({
};
useEffect(() => {
console.log(methods.formState.errors);
}, [methods.formState.errors]);
if (mode === PilotFormMode.VIEW) {
setIsDisabled(true);
}
}, [mode]);
useEffect(() => {
const getPilot = async () => {
try {
setIsLoading(true);
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots/${pilotId}`,
config
);
const pilot = response.data;
console.log(pilot);
// methods.setValue('blah', pilot.value)
methods.reset(pilot);
console.log(methods.getValues());
} catch (error) {
} finally {
setIsLoading(false);
}
};
if (pilotId) {
getPilot();
}
}, [pilotId]);
return (
<Drawer
@@ -122,7 +168,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
data-testid="pilot-drawer"
>
<FormProvider {...methods}>
<DrawerHeader text="Add Pilot" onClose={onOpenCloseDrawer} />
<DrawerHeader text="Add Pilot" onClose={onCancel} />
<form onSubmit={methods.handleSubmit(onSubmit)}>
<DrawerBody>
<div className="grid grid-cols-4 gap-4">
@@ -134,11 +180,11 @@ const PilotForm: React.FC<IPilotFormProps> = ({
name="name"
control={methods.control}
rules={{ required: 'A name must be selected' }}
render={({ field: { disabled, value } }) => (
render={({ field: { value } }) => (
<PeoplePicker
results={peoplePickerResults}
inputProps={{
disabled: disabled,
disabled: isDisabled,
labelProps: {
className: 'before:content-none after:content-none'
},
@@ -159,178 +205,209 @@ const PilotForm: React.FC<IPilotFormProps> = ({
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Address *</Typography>
</div>
<div className="col-span-3">
<Controller
name="address"
control={methods.control}
rules={{ required: 'An address is required' }}
render={({ field: { disabled, onChange, value } }) => (
<Input
className="!border-t-blue-gray-200 focus:!border-t-gray-900"
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">Address *</Typography>
</div>
<div className="col-span-3">
<Controller
name="address"
control={methods.control}
rules={{ required: 'An address is required' }}
render={({ field: { onChange, value } }) => (
<Input
className="!border-t-blue-gray-200 focus:!border-t-gray-900"
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={
methods.formState.errors.address ? true : false
}
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-address-input"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">City *</Typography>
</div>
<div className="col-span-3">
<Controller
name="city"
control={methods.control}
rules={{ required: 'A city is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.city ? true : false}
helperText={
methods.formState.errors.city
? methods.formState.errors.city.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-city-input"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">State *</Typography>
</div>
<div className="col-span-3">
<Controller
name="state"
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { onChange, value } }) => (
<StateSelect
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.state ? true : false}
helperText={
methods.formState.errors.state
? methods.formState.errors.state.message?.toString()
: undefined
}
onChange={onChange}
value={value}
variant="outlined"
data-testid="pilot-form-state-dropdown"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">Postal Code *</Typography>
</div>
<div className="col-span-3">
<Controller
name="postalCode"
control={methods.control}
rules={{ required: 'A postal code is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={
methods.formState.errors.postalCode ? true : false
}
helperText={
methods.formState.errors.postalCode
? methods.formState.errors.postalCode.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-postal-code-input"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">Email</Typography>
</div>
<div className="col-span-3">
<Controller
name="email"
control={methods.control}
rules={{
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
}}
error={methods.formState.errors.address ? true : false}
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-address-input"
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.email ? true : false}
helperText={
methods.formState.errors.email
? methods.formState.errors.email.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-email-input"
/>
)}
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">City *</Typography>
</div>
<div className="col-span-3">
<Controller
name="city"
control={methods.control}
rules={{ required: 'A city is required' }}
render={({ field: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">Phone Number</Typography>
</div>
<div className="col-span-3">
<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'
}
}}
error={methods.formState.errors.city ? true : false}
helperText={
methods.formState.errors.city
? methods.formState.errors.city.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-city-input"
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.phone ? true : false}
helperText={
methods.formState.errors.phone
? methods.formState.errors.phone.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-phone-input"
/>
)}
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">State *</Typography>
</div>
<div className="col-span-3">
<Controller
name="state"
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { disabled, onChange, value } }) => (
<StateSelect
disabled={disabled}
error={methods.formState.errors.state ? true : false}
helperText={
methods.formState.errors.state
? methods.formState.errors.state.message?.toString()
: undefined
}
onChange={onChange}
value={value}
variant="outlined"
data-testid="pilot-form-state-dropdown"
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Postal Code *</Typography>
</div>
<div className="col-span-3">
<Controller
name="postalCode"
control={methods.control}
rules={{ required: 'A postal code is required' }}
render={({ field: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.postalCode ? true : false}
helperText={
methods.formState.errors.postalCode
? methods.formState.errors.postalCode.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-postal-code-input"
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Email</Typography>
</div>
<div className="col-span-3">
<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: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.email ? true : false}
helperText={
methods.formState.errors.email
? methods.formState.errors.email.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-email-input"
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Phone Number</Typography>
</div>
<div className="col-span-3">
<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: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.phone ? true : false}
helperText={
methods.formState.errors.phone
? methods.formState.errors.phone.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-phone-input"
/>
)}
/>
</div>
{pilotId && (
</div>
</>
)}
{/* {pilotId && (
<>
<div className="col-span-1">
<Typography variant="h6">Last Review</Typography>
@@ -354,8 +431,8 @@ const PilotForm: React.FC<IPilotFormProps> = ({
/>
</div>
</>
)}
{pilotId && (
)} */}
{/* {pilotId && (
<>
<div className="col-span-4">
<Typography variant="h5">Medical</Typography>
@@ -434,35 +511,41 @@ const PilotForm: React.FC<IPilotFormProps> = ({
</div>
<PilotFormEndorsements endorsements={[]} />
</>
)}
)} */}
</div>
</DrawerBody>
<DrawerFooter>
<div className="flex gap-2 justify-end justify-self-center pt-4">
<div>
<Button
className="flex items-center gap-3"
variant="outlined"
onClick={onOpenCloseDrawer}
data-testid="pilot-cancel-button"
>
<XmarkIcon size="lg" />
Cancel
</Button>
</div>
<div>
<Button
className="flex items-center gap-3"
loading={isLoading}
variant="filled"
type="submit"
data-testid="pilot-save-button"
>
<SaveIcon size="lg" />
Save
</Button>
</div>
</div>
<>
{mode !== PilotFormMode.VIEW && (
<div className="flex gap-2 justify-end justify-self-center pt-4">
<div>
<Button
className="flex items-center gap-3"
disabled={isDisabled}
variant="outlined"
onClick={onCancel}
data-testid="pilot-cancel-button"
>
<XmarkIcon size="lg" />
Cancel
</Button>
</div>
<div>
<Button
className="flex items-center gap-3"
disabled={isDisabled}
loading={isLoading}
variant="filled"
type="submit"
data-testid="pilot-save-button"
>
<SaveIcon size="lg" />
Save
</Button>
</div>
</div>
)}
</>
</DrawerFooter>
</form>
</FormProvider>

View File

@@ -19,14 +19,34 @@ import {
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { PilotFormMode } from '../pilotForm/PilotForm';
const Pilots: React.FC<unknown> = () => {
const httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [pilotFormMode, setPilotFormMode] = useState<PilotFormMode>(
PilotFormMode.CANCEL
);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenCloseDrawer = () => {
setIsDrawerOpen(!isDrawerOpen);
const onOpenClosePilotForm = (mode: PilotFormMode, pilotId?: string) => {
switch (mode) {
case PilotFormMode.ADD:
case PilotFormMode.EDIT:
case PilotFormMode.VIEW:
setPilotFormMode(mode);
setSelectedPilotId(pilotId);
setIsDrawerOpen(true);
break;
case PilotFormMode.CANCEL:
setPilotFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
break;
}
};
type Pilot = {
@@ -40,54 +60,66 @@ const Pilots: React.FC<unknown> = () => {
{
accessorKey: 'name',
header: 'Name'
},
{
id: 'actions',
header: 'Actions',
cellProps: {
className: 'text-end'
},
cell: (info: any) => {
const pilotId = info.row.original.rowKey;
return (
<Menu placement="bottom-end">
<MenuHandler>
<div>
<IconButton variant="text">
<EllipsisVerticalIcon size="xl" />
</IconButton>
</div>
</MenuHandler>
<MenuList>
<MenuItem
className="flex gap-3"
onClick={() =>
onOpenClosePilotForm(PilotFormMode.EDIT, pilotId)
}
>
<PenIcon size="lg" />
Edit
</MenuItem>
<MenuItem
className="flex gap-3"
onClick={() =>
onOpenClosePilotForm(PilotFormMode.VIEW, pilotId)
}
>
<EyeIcon size="lg" />
View
</MenuItem>
<hr className="my-3" />
<MenuItem className="flex gap-3">
<TrashIcon size="lg" />
Delete
</MenuItem>
</MenuList>
</Menu>
);
},
enableSorting: false
}
// {
// id: 'actions',
// header: 'Actions',
// cellProps: {
// className: 'text-center'
// },
// cell: () => {
// return (
// <Menu placement="bottom-end">
// <MenuHandler>
// <div>
// <IconButton variant="text">
// <EllipsisVerticalIcon size="xl" />
// </IconButton>
// </div>
// </MenuHandler>
// <MenuList>
// <MenuItem className="flex gap-3">
// <PenIcon size="lg" />
// Edit
// </MenuItem>
// <MenuItem className="flex gap-3">
// <EyeIcon size="lg" />
// View
// </MenuItem>
// <hr className="my-3" />
// <MenuItem className="flex gap-3">
// <TrashIcon size="lg" />
// Delete
// </MenuItem>
// </MenuList>
// </Menu>
// );
// },
// enableSorting: false
// }
];
useEffect(() => {
const getPilots = async () => {
try {
const accessToken: string = await getAccessToken();
const response: AxiosResponse = await httpClient.get(`api/pilots`, {
headers: {
Authorization: accessToken
}
});
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots`,
config
);
console.log(response.data);
setPilots(response.data);
} catch (error) {
@@ -108,7 +140,7 @@ const Pilots: React.FC<unknown> = () => {
<Button
className="flex justify-center gap-3"
variant="filled"
onClick={onOpenCloseDrawer}
onClick={() => onOpenClosePilotForm(PilotFormMode.ADD)}
data-testid="pilot-add-button"
>
<PlusIcon size="lg" />
@@ -119,7 +151,9 @@ const Pilots: React.FC<unknown> = () => {
</div>
<PilotForm
isDrawerOpen={isDrawerOpen}
onOpenCloseDrawer={onOpenCloseDrawer}
mode={pilotFormMode}
onOpenClose={(mode) => onOpenClosePilotForm(mode)}
pilotId={selectedPilotId}
/>
</>
);

View File

@@ -3,6 +3,7 @@ import { ISiteNavProps } from './ISiteNavProps';
// import { Link as ReactRouterLink } from 'react-router-dom';
import { useAppContext } from '../../hooks/appContext/UseAppContext';
import {
Avatar,
Button,
Menu,
MenuHandler,
@@ -160,24 +161,21 @@ const SiteNav: React.FC<unknown> = () => {
{!loading && isAuthenticated && (
<Menu placement="bottom-end">
<MenuHandler>
<>
{/* {userPhoto && */}
{/* <img className='rounded-full' src={userPhoto} /> */}
{/* } */}
{/* {!userPhoto && */}
<div className="rounded-full text-white text-center pt-2 bg-black h-[40px] w-[40px]">
NS
</div>
{/* <div className="flex gap-2">
<div className='flex-none'>
</div>
<Button className='flex-1' variant="text" size="sm">
{appContext.state.userProfile.displayName}
</Button>
</div> */}
</>
<div>
{userPhoto && (
<img
className="rounded-full cursor-pointer"
height="40"
width="40"
src={userPhoto}
/>
)}
{!userPhoto && (
<div className="rounded-full cursor-pointer text-white text-center pt-2 bg-black h-[40px] w-[40px]">
NS
</div>
)}
</div>
</MenuHandler>
<MenuList>
<MenuItem onClick={handleSignOut}>