diff --git a/src/components/CurrentlyOutEquipment.tsx b/src/components/CurrentlyOutEquipment.tsx new file mode 100644 index 00000000..863d3738 --- /dev/null +++ b/src/components/CurrentlyOutEquipment.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; +import useSwr from 'swr'; +import { getResponseContentOrError } from '../lib/utils'; +import { CurrentlyOutEquipmentInfo } from '../models/misc/CurrentlyOutEquipmentInfo'; +import { TableConfiguration, TableDisplay } from './TableDisplay'; +import { Card } from 'react-bootstrap'; +import TableStyleLink from './utils/TableStyleLink'; + +const EquipmentNameDisplayFn = (x: CurrentlyOutEquipmentInfo) => + x.equipmentId ? {x.name} : {x.name}; + +const CurrentlyOutEquipment: React.FC = () => { + const { data: currentlyOutEquipmentInfos } = useSwr('/api/equipment/currentlyOut', (url) => + fetch(url).then((response) => getResponseContentOrError(response)), + ); + + if (!currentlyOutEquipmentInfos) { + return ; + } + + const tableSettings: TableConfiguration = { + entityTypeDisplayName: 'utrustning', + defaultSortPropertyName: 'name', + defaultSortAscending: true, + hideTableFilter: true, + hideTableCountControls: true, + columns: [ + { + key: 'name', + displayName: 'Utrustning', + getValue: (x: CurrentlyOutEquipmentInfo) => x.name, + getContentOverride: EquipmentNameDisplayFn, + textTruncation: true, + columnWidth: 300, + }, + { + key: 'numberOfUnits', + displayName: 'Antal', + getValue: (x: CurrentlyOutEquipmentInfo) => x.numberOfUnits, + columnWidth: 220, + }, + ], + }; + + return ( + + Utlämnad utrustning + + + ); +}; + +export default CurrentlyOutEquipment; diff --git a/src/lib/db-access/equipmentList.ts b/src/lib/db-access/equipmentList.ts index d01acf82..7303063f 100644 --- a/src/lib/db-access/equipmentList.ts +++ b/src/lib/db-access/equipmentList.ts @@ -8,6 +8,10 @@ import { import { ensureDatabaseIsInitialized } from '../database'; import { validateEquipmentListEntryObjectionModel } from './equipmentListEntry'; import { compareLists, removeIdAndDates, withCreatedDate, withUpdatedDate } from './utils'; +import { RentalStatus } from '../../models/enums/RentalStatus'; +import { BookingType } from '../../models/enums/BookingType'; +import { formatDatetimeForForm } from '../datetimeUtils'; +import { Status } from '../../models/enums/Status'; export const fetchEquipmentList = async ( id: number, @@ -41,6 +45,31 @@ export const fetchEquipmentListsForBooking = async (bookingId: number): Promise< return EquipmentListObjectionModel.query().where('bookingId', bookingId).orderBy('id'); }; +export const fetchOutEquipmentLists = async (): Promise => { + ensureDatabaseIsInitialized(); + const now = formatDatetimeForForm(new Date()); + + return EquipmentListObjectionModel.query() + .join('Booking', 'Booking.id', '=', 'EquipmentList.bookingId') + .where({ rentalStatus: RentalStatus.OUT, bookingType: BookingType.RENTAL }) + .orWhere((x) => + x + .where({ bookingType: BookingType.GIG }) + .where('equipmentOutDatetime', '<=', now) + .where('equipmentInDatetime', '>=', now) + .whereNot({status: Status.CANCELED}) + ) + .orWhere((x) => + x + .where({ bookingType: BookingType.GIG }) + .where('usageStartDatetime', '<=', now) + .where('usageEndDatetime', '>=', now) + .whereNot({status: Status.CANCELED}) + ) + .withGraphFetched('listEntries.equipment') + .withGraphFetched('listHeadings.listEntries.equipment'); +}; + export const updateEquipmentList = async ( id: number, equipmentList: EquipmentListObjectionModel, diff --git a/src/models/misc/CurrentlyOutEquipmentInfo.ts b/src/models/misc/CurrentlyOutEquipmentInfo.ts new file mode 100644 index 00000000..4fd99422 --- /dev/null +++ b/src/models/misc/CurrentlyOutEquipmentInfo.ts @@ -0,0 +1,6 @@ +export interface CurrentlyOutEquipmentInfo { + id: number; + equipmentId?: number; + name: string; + numberOfUnits: number; +} diff --git a/src/pages/api/equipment/currentlyOut.ts b/src/pages/api/equipment/currentlyOut.ts new file mode 100644 index 00000000..9866eab6 --- /dev/null +++ b/src/pages/api/equipment/currentlyOut.ts @@ -0,0 +1,52 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { respondWithCustomErrorMessage, respondWithEntityNotFoundResponse } from '../../../lib/apiResponses'; +import { withSessionContext } from '../../../lib/sessionContext'; +import { fetchOutEquipmentLists } from '../../../lib/db-access/equipmentList'; +import { groupBy, reduceSumFn } from '../../../lib/utils'; +import { EquipmentListObjectionModel } from '../../../models/objection-models/BookingObjectionModel'; + +const handler = withSessionContext(async (req: NextApiRequest, res: NextApiResponse): Promise | void> => { + switch (req.method) { + case 'GET': + await fetchOutEquipmentLists() + .then(getEquipmentFromLists) + .then((result) => res.status(200).json(result)) + .catch((error) => respondWithCustomErrorMessage(res, error.message)); + + return; + + default: + respondWithEntityNotFoundResponse(res); + } + + return; +}); + +export default handler; + +const getEquipmentFromLists = (equipmentLists: EquipmentListObjectionModel[]) => { + const listEntries = equipmentLists.flatMap((list) => [ + ...list.listEntries, + ...list.listHeadings.flatMap((heading) => heading.listEntries), + ]); + + const equipmentGroupings = groupBy( + listEntries.filter((x) => x.equipmentId), + (x) => x.equipmentId?.toString() ?? '0', + ); + const equipmentWithCount = Object.keys(equipmentGroupings).map((key) => { + const records = equipmentGroupings[key]; + return { + equipmentId: key, + id: records[0].id, + name: records[0].equipment?.name, + numberOfUnits: records.map((x) => x.numberOfUnits).reduce(reduceSumFn, 0), + }; + }); + + const customRows = listEntries + .filter((x) => !x.equipment) + .map((x) => ({ name: x.name, id: x.id, numberOfUnits: x.numberOfUnits })); + + return [...equipmentWithCount, ...customRows]; +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index add251cf..01d60d4c 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -21,6 +21,7 @@ import Link from 'next/link'; import { IfNotReadonly } from '../components/utils/IfAdmin'; import TableStyleLink from '../components/utils/TableStyleLink'; import { KeyValue } from '../models/interfaces/KeyValue'; +import CurrentlyOutEquipment from '../components/CurrentlyOutEquipment'; // eslint-disable-next-line react-hooks/rules-of-hooks export const getServerSideProps = useUserWithDefaultAccessAndWithSettings(); @@ -74,6 +75,7 @@ const IndexPage: React.FC = ({ user: currentUser, globalSettings }: Props bookings={outBookings} showDateHeadings={false} > + Aktivitet