115 add load more button to logbook mobile view #117

Merged
noahspannbauer merged 3 commits from 115-add-load-more-button-to-logbook-mobile-view into main 2026-03-21 21:41:50 -04:00
8 changed files with 128 additions and 70 deletions

View File

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

View File

@@ -20,6 +20,7 @@ import { LogInterceptor } from './log.interceptor';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service';
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm'; import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { Logs } from './logs.interface';
const reflector = new Reflector(); const reflector = new Reflector();
@@ -33,7 +34,7 @@ export class LogController {
@Get() @Get()
@Public() @Public()
async findLogsWithCount(@Query('skip') skip?, @Query('take') take?: number,): Promise<{ entities: LogEntity[], total: number, hasNextPage: boolean }> { async findLogsWithCount(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
try { try {
return await this.logService.findLogsWithCount(skip, take) return await this.logService.findLogsWithCount(skip, take)
} catch (error) { } catch (error) {
@@ -43,13 +44,11 @@ export class LogController {
} }
} }
@Get('tracks') @Get('flights')
@Public() @Public()
async findLogsWithTracks(@Query('skip') skip?, @Query('take') take?: number,): Promise<{ entities: LogEntity[], total: number, hasNextPage: boolean }> { async findLogsWithTracks(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
try { try {
const result = await this.logService.findLogsWithTracks(skip, take) return await this.logService.findLogsWithTracks(skip, take)
console.log(result);
return result
} catch (error) { } catch (error) {
const customError = error as CustomError; const customError = error as CustomError;

View File

@@ -15,10 +15,9 @@ export class LogInterceptor implements NestInterceptor {
]); ]);
return handler.handle().pipe( return handler.handle().pipe(
map((data: { entities: LogEntity[], total: number, hasNextPage: boolean }) => { map((data: any) => {
const req = context.switchToHttp().getRequest(); const req = context.switchToHttp().getRequest();
const limitData = (entities: LogEntity[]) => { const limitData = (log: LogEntity) => {
return entities.map((log: LogEntity) => {
return { return {
id: log.id, id: log.id,
pilot: { pilot: {
@@ -31,7 +30,6 @@ export class LogInterceptor implements NestInterceptor {
durationOfFlight: log.durationOfFlight, durationOfFlight: log.durationOfFlight,
tracks: log.tracks, tracks: log.tracks,
}; };
});
} }
if (req.headers.authorization) { if (req.headers.authorization) {
@@ -41,25 +39,39 @@ export class LogInterceptor implements NestInterceptor {
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles')); const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
if (jwtPayload[rolesKeyName].includes('Flying.Read')) { if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
const logs = limitData(data.entities); if (data.entities) {
const logs = data.entities.map((entity) => limitData(data.entities));
return { return {
entities: logs, entities: logs,
total: data.total, total: data.total,
hasNextPage: data.hasNextPage hasNextPage: data.hasNextPage
}; };
} else {
const log = limitData(data);
return log;
}
} else { } else {
return data; return data;
} }
} else if (!req.headers.authorization && isPublic) { } else if (!req.headers.authorization && isPublic) {
const publicData = limitData(data.entities) if (data.entities) {
const publicData = data.entities.map((entity) => limitData(entity))
const logs = publicData.slice(0, 5) const logs = publicData.slice(0, 5)
return { return {
entities: logs, entities: publicData,
total: data.total, total: data.total,
hasNextPage: data.hasNextPage hasNextPage: false
}; };
} else {
const publicData = limitData(data);
return publicData
}
} }
}) })
); );

View File

@@ -7,6 +7,7 @@ import { PilotService } from '../pilot/pilot.service';
import { PilotEntity } from '../pilot/pilot.entity'; import { PilotEntity } from '../pilot/pilot.entity';
import { CustomError } from '../error/customError'; import { CustomError } from '../error/customError';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service';
import { Logs } from './logs.interface';
@Injectable() @Injectable()
export class LogService { export class LogService {
@@ -17,15 +18,13 @@ export class LogService {
) {} ) {}
async find(id: string): Promise<LogEntity> { async find(id: string): Promise<LogEntity> {
const logEntity: LogEntity = await this.logRepository.findOne({ return await this.logRepository.findOne({
where: { id: id }, where: { id: id },
relations: ['pilot', 'tracks'] relations: ['pilot', 'tracks']
}); });
return logEntity;
} }
async findLogsWithCount(skip?: number, take?: number): Promise<{entities: LogEntity[], total: number, hasNextPage: boolean}> { async findLogsWithCount(skip?: number, take?: number): Promise<Logs> {
const [entities, total] = await this.logRepository.findAndCount({ const [entities, total] = await this.logRepository.findAndCount({
take, take,
skip, skip,
@@ -39,7 +38,7 @@ export class LogService {
} }
} }
async findLogsWithTracks(skip?: number, take?: number): Promise<{entities: LogEntity[], total: number, hasNextPage: boolean}> { async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
const [entities, total] = await this.logRepository const [entities, total] = await this.logRepository
.createQueryBuilder('logs') .createQueryBuilder('logs')
.innerJoinAndSelect('logs.tracks', 'track') .innerJoinAndSelect('logs.tracks', 'track')

View File

@@ -0,0 +1,7 @@
import { LogEntity } from "src/log/log.entity";
export interface Logs {
entities: LogEntity[],
total: number,
hasNextPage: boolean
}

View File

@@ -7,6 +7,7 @@ import { LogService } from "../log/log.service";
import { LogEntity } from "../log/log.entity"; import { LogEntity } from "../log/log.entity";
import { CustomError } from "../error/customError"; import { CustomError } from "../error/customError";
import { FileService } from "../file/file.service"; import { FileService } from "../file/file.service";
import { Logs } from "src/log/logs.interface";
@Injectable() @Injectable()
export class TrackService { export class TrackService {

View File

@@ -17,7 +17,7 @@ const Flights = () => {
const getFlights = async (pageIndex: number, pageSize: number) => { const getFlights = async (pageIndex: number, pageSize: number) => {
try { try {
const response: AxiosResponse = await httpClient.get(`api/logs/tracks`, { const response: AxiosResponse = await httpClient.get(`api/logs/flights`, {
params: { params: {
skip: pageIndex, skip: pageIndex,
take: pageSize take: pageSize
@@ -61,10 +61,6 @@ const Flights = () => {
getFlights(state.pageIndex, state.pageSize); getFlights(state.pageIndex, state.pageSize);
}, []) }, [])
useEffect(() => {
console.log(screenSize)
}, [screenSize])
return ( return (
<div className={`max-w-screen-lg mx-auto ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD? 'mr-4 ml-4' : ''}`}> <div className={`max-w-screen-lg mx-auto ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD? 'mr-4 ml-4' : ''}`}>
<div className='prose mt-5 mb-5'> <div className='prose mt-5 mb-5'>

View File

@@ -17,6 +17,11 @@ import { CellContext, ColumnDef, flexRender, getCoreRowModel, HeaderContext, Pag
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'; import Alert from '../alert/Alert';
import TrackMap from '../trackMap/TrackMap';
interface ActionsProps {
id: string;
}
const Logbook: React.FC<unknown> = () => { const Logbook: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
@@ -25,6 +30,22 @@ const Logbook: React.FC<unknown> = () => {
const { isUserLoggedIn } = useOidc(); const { isUserLoggedIn } = useOidc();
const { userRole } = useUserRole(); const { userRole } = useUserRole();
const { screenSize } = useBreakpoints(); const { screenSize } = useBreakpoints();
const Actions = ({ id }: ActionsProps) => {
return (
<div className='dropdown dropdown-end'>
<div tabIndex={0} role='button' className='btn btn-ghost p-0'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-sm border border-base-300 !z-[100]">
{isUserLoggedIn && userRole === UserRole.WRITE &&
<li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
}
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
{isUserLoggedIn && userRole === UserRole.WRITE &&
<li><a onClick={() => onDeleteLog(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
}
</ul>
</div>
)
}
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => { const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value)); 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;
@@ -116,18 +137,7 @@ const Logbook: React.FC<unknown> = () => {
}, },
cell: (info: CellContext<LogbookEntry, unknown>) => { cell: (info: CellContext<LogbookEntry, unknown>) => {
return ( return (
<div className='dropdown dropdown-end'> <Actions id={info.row.original.id} />
<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 w-52 p-2 shadow-sm border border-base-300 !z-[100]">
{isUserLoggedIn && userRole === UserRole.WRITE &&
<li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
}
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
{isUserLoggedIn && userRole === UserRole.WRITE &&
<li><a onClick={() => onDeleteLog(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
}
</ul>
</div>
) )
} }
} }
@@ -489,12 +499,11 @@ const Logbook: React.FC<unknown> = () => {
}; };
const onRowsPerPageChanged = (event: any) => { const onRowsPerPageChanged = (event: any) => {
console.log(event.target.value)
const newPaginationState: PaginationState = { const newPaginationState: PaginationState = {
pageIndex: 0, pageIndex: 0,
pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries
}; };
console.log(newPaginationState)
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState }) dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
} }
@@ -520,7 +529,7 @@ const Logbook: React.FC<unknown> = () => {
return ( return (
<> <>
<div className='mr-10 ml-10 grid grid-cols-12'> <div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
<div className='prose max-w-none col-span-10 mt-5 mb-5'> <div className='prose max-w-none col-span-10 mt-5 mb-5'>
<h1>Logbook</h1> <h1>Logbook</h1>
</div> </div>
@@ -547,7 +556,7 @@ const Logbook: React.FC<unknown> = () => {
</Alert> </Alert>
</div> </div>
)} )}
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && ( {!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD && (
<div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg '> <div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg '>
<div className='overflow-x-auto mb-5'> <div className='overflow-x-auto mb-5'>
<div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'> <div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'>
@@ -696,7 +705,47 @@ const Logbook: React.FC<unknown> = () => {
</div> </div>
)} )}
{state.entries.length > 0 && screenSize === ScreenSize.SM && {state.entries.length > 0 && screenSize === ScreenSize.SM &&
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} /> <div className='col-span-12'>
<>
{table.getRowModel().rows.map((row) => {
const date = new Date(row.original.date.replace('Z', ''));
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
return (
<div className='card bg-base-100 border border-base-300 mb-5'>
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
<div className={`grid grid-cols-12 gap-3`}>
<>
<div className='col-span-8'>
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
</div>
<div className='col-span-4 justify-self-end self-center'>
<Actions id={row.original.id} />
</div>
{row.getVisibleCells().map((cell) => {
return (
<>
{cell.column.columnDef.header !== 'Actions' && cell.column.columnDef.header !== 'Date' && cell.column.columnDef.header !== 'Pilot' &&
<>
<div className='col-span-8 font-bold'>
<span>{cell.getContext().column.columnDef.header?.toString()}</span>
</div>
<div className='col-span-4'>
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
</div>
</>
}
</>
)
})}
</>
</div>
</div>
</div>
)
})}
</>
</div>
} }
{state.isLoading && !state.alert && ( {state.isLoading && !state.alert && (
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'> <div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>