diff --git a/app/actions/ActionTypes.ts b/app/actions/ActionTypes.ts index 0060ca9551..3dad4aa4a5 100644 --- a/app/actions/ActionTypes.ts +++ b/app/actions/ActionTypes.ts @@ -9,9 +9,6 @@ export const generateStatuses = (name: string): AsyncActionType => ({ // Create shorthand to make code format cleaner type AAT = AsyncActionType; -/** - * - */ export const Event = { FETCH: generateStatuses('Event.FETCH') as AAT, FETCH_PREVIOUS: generateStatuses('Event.FETCH_PREVIOUS') as AAT, @@ -38,9 +35,6 @@ export const Event = { FETCH_FOLLOWERS: generateStatuses('Event.FETCH_FOLLOWERS') as AAT, }; -/** - * - */ export const Article = { FETCH: generateStatuses('Article.FETCH') as AAT, CREATE: generateStatuses('Article.CREATE') as AAT, @@ -48,36 +42,24 @@ export const Article = { DELETE: generateStatuses('Article.DELETE') as AAT, }; -/** - * - */ export const EmailList = { FETCH: generateStatuses('EmailList.FETCH') as AAT, CREATE: generateStatuses('EmailList.CREATE') as AAT, EDIT: generateStatuses('EmailList.EDIT') as AAT, }; -/** - * - */ export const RestrictedMail = { FETCH: generateStatuses('RestrictedMail.FETCH') as AAT, CREATE: generateStatuses('RestrictedMail.CREATE') as AAT, EDIT: generateStatuses('RestrictedMail.EDIT') as AAT, }; -/** - * - */ export const EmailUser = { FETCH: generateStatuses('EmailUser.FETCH') as AAT, CREATE: generateStatuses('EmailUser.CREATE') as AAT, EDIT: generateStatuses('EmailUser.EDIT') as AAT, }; -/** - * - */ export const Gallery = { FETCH: generateStatuses('Gallery.FETCH') as AAT, CREATE: generateStatuses('Gallery.CREATE') as AAT, @@ -89,9 +71,6 @@ export const ImageGallery = { FETCH_ALL: generateStatuses('ImageGallery.FETCH_ALL') as AAT, }; -/** - * - */ export const GalleryPicture = { FETCH: generateStatuses('GalleryPicture.FETCH') as AAT, FETCH_SIBLING: generateStatuses('GalleryPicture.FETCH_SIBLING') as AAT, @@ -101,9 +80,6 @@ export const GalleryPicture = { UPLOAD: generateStatuses('GalleryPicture.UPLOAD') as AAT, }; -/** - * - */ export const Joblistings = { FETCH: generateStatuses('Joblistings.FETCH') as AAT, CREATE: generateStatuses('Joblistings.CREATE') as AAT, @@ -111,9 +87,6 @@ export const Joblistings = { DELETE: generateStatuses('Joblistings.DELETE') as AAT, }; -/** - * - */ export const Announcements = { FETCH_ALL: generateStatuses('Announcements.FETCH_ALL') as AAT, CREATE: generateStatuses('Announcements.CREATE') as AAT, @@ -121,9 +94,6 @@ export const Announcements = { DELETE: generateStatuses('Announcements.DELETE') as AAT, }; -/** - * - */ export const Meeting = { FETCH: generateStatuses('Meeting.FETCH') as AAT, SET_INVITATION_STATUS: generateStatuses( @@ -137,9 +107,6 @@ export const Meeting = { ) as AAT, }; -/** - * - */ export const Group = { FETCH: generateStatuses('Group.FETCH') as AAT, UPDATE: generateStatuses('Group.UPDATE') as AAT, @@ -163,24 +130,15 @@ export const Membership = { LEAVE_GROUP: generateStatuses('Membership.LEAVE_GROUP') as AAT, }; -/** - * - */ export const Favorite = { FETCH_ALL: generateStatuses('Favorite.FETCH_ALL') as AAT, }; -/** - * - */ export const Comment = { ADD: generateStatuses('Comment.ADD') as AAT, DELETE: generateStatuses('Comment.DELETE') as AAT, }; -/** - * - */ export const Company = { FETCH: generateStatuses('Company.FETCH') as AAT, FETCH_COMPANY_CONTACT: generateStatuses( @@ -204,9 +162,6 @@ export const Company = { EDIT_SEMESTER: generateStatuses('Company.EDIT_SEMESTER') as AAT, }; -/** - * - */ export const Quote = { FETCH: generateStatuses('Quote.FETCH') as AAT, FETCH_ALL_APPROVED: generateStatuses('Quote.FETCH_ALL_APPROVED') as AAT, @@ -218,9 +173,6 @@ export const Quote = { ADD: generateStatuses('Quote.ADD') as AAT, }; -/** - * - */ export const Search = { SEARCH: generateStatuses('Search.SEARCH') as AAT, AUTOCOMPLETE: generateStatuses('Search.AUTOCOMPLETE') as AAT, @@ -234,9 +186,6 @@ export const NotificationsFeed = { MARK: generateStatuses('NotificationsFeed.MARK') as AAT, }; -/** - * - */ export const User = { FETCH: generateStatuses('User.FETCH') as AAT, UPDATE: generateStatuses('User.UPDATE') as AAT, @@ -266,9 +215,6 @@ export const Penalty = { DELETE: generateStatuses('Penalty.DELETE') as AAT, }; -/** - * - */ export const Page = { FETCH: generateStatuses('Page.FETCH') as AAT, CREATE: generateStatuses('Page.CREATE') as AAT, @@ -276,16 +222,10 @@ export const Page = { DELETE: generateStatuses('Page.DELETE') as AAT, }; -/** - * - */ export const Bdb = { FETCH: generateStatuses('Bdb.FETCH') as AAT, }; -/** - * - */ export const Survey = { FETCH: generateStatuses('Survey.FETCH') as AAT, ADD: generateStatuses('Survey.ADD') as AAT, @@ -294,9 +234,6 @@ export const Survey = { HIDE: generateStatuses('Survey.HIDE') as AAT, }; -/** - * - */ export const SurveySubmission = { FETCH_ALL: generateStatuses('SurveySubmission.FETCH_ALL') as AAT, FETCH: generateStatuses('SurveySubmission.FETCH') as AAT, @@ -310,25 +247,16 @@ export const Emoji = { FETCH_ALL: generateStatuses('Emoji.FETCH_ALL') as AAT, }; -/** - * - */ export const File = { FETCH_SIGNED_POST: generateStatuses('File.FETCH_SIGNED_POST') as AAT, UPLOAD: generateStatuses('File.UPLOAD') as AAT, PATCH: generateStatuses('File.PATCH') as AAT, }; -/** - * - */ export const Feed = { FETCH: generateStatuses('Feed.FETCH') as AAT, }; -/** - * - */ export const OAuth2 = { FETCH_APPLICATIONS: generateStatuses('OAuth2.FETCH_APPLICATIONS') as AAT, FETCH_APPLICATION: generateStatuses('OAuth2.FETCH_APPLICATION') as AAT, @@ -338,9 +266,6 @@ export const OAuth2 = { DELETE_GRANT: generateStatuses('OAuth2.DELETE_GRANT') as AAT, }; -/** - * - */ export const NotificationSettings = { FETCH_ALTERNATIVES: generateStatuses( 'NotificationSettings.FETCH_ALTERNATIVES', @@ -349,16 +274,10 @@ export const NotificationSettings = { UPDATE: generateStatuses('NotificationSettings.UPDATE') as AAT, }; -/** - * - */ export const Contact = { SEND_MESSAGE: generateStatuses('Contact.SEND_MESSAGE') as AAT, }; -/** - * - */ export const Meta = { FETCH: generateStatuses('Meta.FETCH') as AAT, }; @@ -382,14 +301,27 @@ export const Poll = { UPDATE: generateStatuses('Poll.UPDATE') as AAT, }; -/** - * - */ export const Reaction = { ADD: generateStatuses('Reaction.ADD') as AAT, DELETE: generateStatuses('Reaction.DELETE') as AAT, }; +export const LendableObject = { + FETCH_ALL: generateStatuses('LendableObject.FETCH_ALL') as AAT, + FETCH: generateStatuses('LendableObject.FETCH') as AAT, + CREATE: generateStatuses('LendableObject.CREATE') as AAT, + EDIT: generateStatuses('LendableObject.EDIT') as AAT, + DELETE: generateStatuses('LendableObject.DELETE') as AAT, +}; + +export const LendingRequest = { + FETCH_ALL: generateStatuses('LendingRequest.FETCH_ALL') as AAT, + FETCH: generateStatuses('LendingRequest.FETCH') as AAT, + CREATE: generateStatuses('LendingRequest.CREATE') as AAT, + EDIT: generateStatuses('LendingRequest.EDIT') as AAT, + DELETE: generateStatuses('LendingRequest.DELETE') as AAT, +}; + export const Forum = { FETCH_ALL: generateStatuses('Forum.FETCH_ALL') as AAT, CREATE: generateStatuses('Forum.CREATE') as AAT, diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts new file mode 100644 index 0000000000..d97321f47f --- /dev/null +++ b/app/actions/LendableObjectActions.ts @@ -0,0 +1,69 @@ +import callAPI from 'app/actions/callAPI'; +import { lendableObjectSchema } from 'app/reducers'; +import { LendableObject } from './ActionTypes'; +import type { EntityId } from '@reduxjs/toolkit'; +import type { + DetailedLendableObject, + ListLendableObject, +} from 'app/store/models/LendableObject'; + +export function fetchAllLendableObjects() { + return callAPI({ + types: LendableObject.FETCH, + endpoint: '/lendableobject/', + schema: [lendableObjectSchema], + meta: { + errorMessage: 'Henting av utlånsobjekter feilet', + }, + propagateError: true, + }); +} + +export function fetchLendableObject(id: EntityId) { + return callAPI({ + types: LendableObject.FETCH, + endpoint: `/lendableobject/${id}/`, + schema: lendableObjectSchema, + meta: { + errorMessage: 'Henting av utlånsobjekt feilet', + }, + propagateError: true, + }); +} + +export function deleteLendableObject(id: EntityId) { + return callAPI({ + types: LendableObject.DELETE, + endpoint: `/lendableobject/${id}/`, + method: 'DELETE', + meta: { + id, + errorMessage: 'Sletting av utlånsobjekt feilet', + }, + }); +} + +export function createLendableObject(data) { + return callAPI({ + types: LendableObject.CREATE, + endpoint: '/lendableobject/', + method: 'POST', + body: data, + schema: lendableObjectSchema, + meta: { + errorMessage: 'Opprettelse av utlånsobjekt feilet', + }, + }); +} + +export function editLendableObject({ id, ...data }) { + return callAPI({ + types: LendableObject.EDIT, + endpoint: `/lendableobject/${id}/`, + method: 'PATCH', + body: data, + meta: { + errorMessage: 'Endring av utlånsobjekt feilet', + }, + }); +} diff --git a/app/actions/LendingRequestActions.ts b/app/actions/LendingRequestActions.ts new file mode 100644 index 0000000000..168d041fbb --- /dev/null +++ b/app/actions/LendingRequestActions.ts @@ -0,0 +1,54 @@ +import { LendingRequest } from 'app/actions/ActionTypes'; +import callAPI from 'app/actions/callAPI'; +import { lendingRequestSchema } from 'app/reducers'; +import type { EntityId } from '@reduxjs/toolkit'; +import type { LendingRequest as LendingRequestModel } from 'app/store/models/LendingRequest'; + +export function fetchAllLendingRequests() { + return callAPI({ + types: LendingRequest.FETCH, + endpoint: '/lendinginstance/', + schema: [lendingRequestSchema], + meta: { + errorMessage: 'Henting av utlånsforespørsler feilet', + }, + propagateError: true, + }); +} + +export function fetchLendingRequest(id: EntityId) { + return callAPI({ + types: LendingRequest.FETCH, + endpoint: `/lendinginstance/${id}/`, + schema: lendingRequestSchema, + meta: { + errorMessage: 'Henting av utlånsforespørsel feilet', + }, + }); +} + +export function fetchLendingRequestsForLendableObject( + lendableObjectId: EntityId, +) { + return callAPI({ + types: LendingRequest.FETCH, + endpoint: `/lendableobject/${lendableObjectId}/lendinginstances/`, + schema: [lendingRequestSchema], + meta: { + errorMessage: 'Henting av utlånsforespørsler feilet', + }, + }); +} + +export function createLendingRequest(data: any) { + return callAPI({ + types: LendingRequest.CREATE, + endpoint: '/lendinginstance/', + method: 'POST', + body: data, + schema: lendingRequestSchema, + meta: { + errorMessage: 'Opprettelse av utlånsforespørsel feilet', + }, + }); +} diff --git a/app/reducers/index.ts b/app/reducers/index.ts index 5cee9d77f3..9273fde290 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -154,3 +154,15 @@ export const threadSchema = new schema.Entity(EntityType.Thread, { export const forumSchema = new schema.Entity(EntityType.Forums, { threads: [threadSchema], }); +export const lendableObjectSchema = new schema.Entity( + EntityType.LendableObjects, + { + responsibleGroups: [groupSchema], + }, +); +export const lendingRequestSchema = new schema.Entity( + EntityType.LendingRequests, + { + lendableObject: lendableObjectSchema, + }, +); diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts new file mode 100644 index 0000000000..e60ca33083 --- /dev/null +++ b/app/reducers/lendableObjects.ts @@ -0,0 +1,23 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { LendableObject } from 'app/actions/ActionTypes'; +import { EntityType } from 'app/store/models/entities'; +import createLegoAdapter from 'app/utils/legoAdapter/createLegoAdapter'; +import type { RootState } from 'app/store/createRootReducer'; + +const legoAdapter = createLegoAdapter(EntityType.LendableObjects); + +const lendableObjectsSlice = createSlice({ + name: EntityType.LendableObjects, + initialState: legoAdapter.getInitialState(), + reducers: {}, + extraReducers: legoAdapter.buildReducers({ + fetchActions: [LendableObject.FETCH_ALL], + deleteActions: [LendableObject.DELETE], + }), +}); + +export default lendableObjectsSlice.reducer; +export const { + selectById: selectLendableObjectById, + selectAll: selectAllLendableObjects, +} = legoAdapter.getSelectors((state: RootState) => state.lendableObjects); diff --git a/app/reducers/lendingRequests.ts b/app/reducers/lendingRequests.ts new file mode 100644 index 0000000000..8fc38288d0 --- /dev/null +++ b/app/reducers/lendingRequests.ts @@ -0,0 +1,28 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { LendingRequest } from 'app/actions/ActionTypes'; +import { EntityType } from 'app/store/models/entities'; +import createLegoAdapter from 'app/utils/legoAdapter/createLegoAdapter'; +import type { RootState } from 'app/store/createRootReducer'; + +const legoAdapter = createLegoAdapter(EntityType.LendingRequests); + +const lendingRequestsSlice = createSlice({ + name: EntityType.LendingRequests, + initialState: legoAdapter.getInitialState(), + reducers: {}, + extraReducers: legoAdapter.buildReducers({ + fetchActions: [LendingRequest.FETCH_ALL], + deleteActions: [LendingRequest.DELETE], + }), +}); + +export default lendingRequestsSlice.reducer; + +export const { + selectById: selectLendingRequestById, + selectAll: selectAllLendingRequests, + selectByField: selectLendingRequestsByField, +} = legoAdapter.getSelectors((state: RootState) => state.lendingRequests); + +export const selectLendingRequestsByLendableObjectId = + selectLendingRequestsByField('lendableObject'); diff --git a/app/routes/index.tsx b/app/routes/index.tsx index d57f635f49..9f22a7f4cc 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -13,6 +13,7 @@ import eventsRoute from './events'; import forumRoute from './forum'; import interestGroupsRoute from './interestgroups'; import joblistingsRoute from './joblistings'; +import lendingRoute from './lending'; import meetingsRoute from './meetings'; import pageNotFound from './pageNotFound'; import pagesRoute from './pages'; @@ -53,6 +54,7 @@ export const routerConfig: RouteObject[] = [ { path: 'interest-groups/*', children: interestGroupsRoute }, { path: 'interestgroups/*', children: interestGroupsRoute }, { path: 'joblistings/*', children: joblistingsRoute }, + { path: 'lending/*', children: lendingRoute }, { path: 'meetings/*', children: meetingsRoute }, { path: 'pages/*', children: pagesRoute }, { path: 'photos/*', children: photosRoute }, diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx new file mode 100644 index 0000000000..11d60ca881 --- /dev/null +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -0,0 +1,83 @@ +import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import { LoadingIndicator, LoadingPage, Page } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { Helmet } from 'react-helmet-async'; +import { useParams } from 'react-router-dom'; +import { fetchLendableObject } from 'app/actions/LendableObjectActions'; +import { fetchAllLendingRequests } from 'app/actions/LendingRequestActions'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; + +const LendableObjectAdminDetail = () => { + const { lendableObjectId } = useParams<{ lendableObjectId: string }>(); + + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchLendableObject', + () => lendableObjectId && dispatch(fetchLendableObject(lendableObjectId)), + [lendableObjectId], + ); + + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, lendableObjectId), + ); + + usePreparedEffect( + 'fetchRequests', + () => dispatch(fetchAllLendingRequests()), + [], + ); + + const lendingRequests = useAppSelector(selectAllLendingRequests); + + const fetchingRequests = useAppSelector( + (state) => state.lendingRequests.fetching, + ); + + if (!lendableObject) { + return ; + } + + const title = `Admin: Godkjenn utlån av ${lendableObject.title}`; + return ( + + + + + + + ); +}; + +export default LendableObjectAdminDetail; diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx new file mode 100644 index 0000000000..baa37f0d18 --- /dev/null +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -0,0 +1,146 @@ +import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import { LinkButton, Modal, Page, PageCover } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import moment from 'moment-timezone'; +import { useState } from 'react'; +import { Field } from 'react-final-form'; +import { Helmet } from 'react-helmet-async'; +import { useNavigate, useParams } from 'react-router-dom'; +import { fetchLendableObject } from 'app/actions/LendableObjectActions'; +import { createLendingRequest } from 'app/actions/LendingRequestActions'; +import DisplayContent from 'app/components/DisplayContent'; +import { TextArea, TextInput } from 'app/components/Form'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import SubmitButton from 'app/components/Form/SubmitButton'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; + +const LendableObjectDetail = () => { + const { lendableObjectId } = useParams<{ lendableObjectId: string }>(); + const [showLendingForm, setShowLendingForm] = useState(false); + const [start, setStart] = useState(''); + const [end, setEnd] = useState(''); + const navigate = useNavigate(); + + const onSubmit = (values) => { + values = { + ...values, + startDate: moment(values.startDate).toISOString(), + endDate: moment(values.endDate).toISOString(), + }; + + dispatch( + createLendingRequest({ + ...values, + pending: false, + lendableObject: lendableObjectId, + }), + ).then(() => navigate('/lending')); + }; + + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchLendableObject', + () => lendableObjectId && dispatch(fetchLendableObject(lendableObjectId)), + [lendableObjectId], + ); + + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, lendableObjectId), + ); + + const fetchingObjects = useAppSelector( + (state) => state.lendableObjects.fetching, + ); + + const initialValues = { + startDate: moment(start).toISOString(), + endDate: moment(end).toISOString(), + }; + + const title = `Utlån av ${lendableObject?.title}`; + return ( + } + actionButtons={ + + Rediger + + } + skeleton={fetchingObjects} + > + + + {lendableObject && 'description' in lendableObject && ( + + )} + + { + setStart(info.startStr); + setEnd(info.endStr); + setShowLendingForm(true); + }} + /> + + + + {({ handleSubmit }) => { + return ( +
+ + + + Send inn forespørsel + + ); + }} +
+
+
+ ); +}; + +export default LendableObjectDetail; diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx new file mode 100644 index 0000000000..272d05ecd8 --- /dev/null +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -0,0 +1,177 @@ +import { + ButtonGroup, + ConfirmModal, + LoadingIndicator, + Page, +} from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { Field } from 'react-final-form'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + createLendableObject, + deleteLendableObject, + editLendableObject, + fetchLendableObject, +} from 'app/actions/LendableObjectActions'; +import { + Button, + EditorField, + Form, + SelectInput, + SubmitButton, + TextInput, +} from 'app/components/Form'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import SubmissionError from 'app/components/Form/SubmissionError'; +import { selectAllGroups } from 'app/reducers/groups'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { roleOptions } from 'app/utils/constants'; +import type { + DetailedLendableObject, + EditingLendableObject, +} from 'app/store/models/LendableObject'; + +const TypedLegoForm = LegoFinalForm; + +const LendableObjectEdit = () => { + const { lendableObjectId } = useParams<{ lendableObjectId: string }>(); + const isNew = lendableObjectId === undefined; + + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + usePreparedEffect( + 'fetchLendableObject', + () => lendableObjectId && dispatch(fetchLendableObject(lendableObjectId)), + [lendableObjectId], + ); + + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, lendableObjectId), + ); + + const groups = useAppSelector(selectAllGroups); + + const onSubmit = (values: EditingLendableObject) => { + if (isNew) { + dispatch( + createLendableObject({ + ...values, + responsibleGroups: values.responsibleGroups?.map((group) => group.id), + responsibleRoles: values.responsibleRoles?.map((role) => role.value), + }), + ).then(() => navigate('/lending')); + } else { + dispatch( + editLendableObject({ + ...values, + id: lendableObjectId, + responsibleGroups: values.responsibleGroups.map( + (group) => group.id || group.value, + ), + responsibleRoles: values.responsibleRoles.map((role) => role.value), + }), + ).then(() => navigate(`/lending/${lendableObjectId}`)); + } + }; + + const onDelete = async () => { + if (!lendableObjectId) { + return; + } + await dispatch(deleteLendableObject(lendableObjectId)); + navigate('/lending'); + }; + + const initialValues = !isNew + ? { + ...lendableObject, + responsibleRoles: lendableObject?.responsibleRoles.map((role) => ({ + label: roleOptions.find((r) => r.value === role)?.label || role, + value: role, + })), + responsibleGroups: (lendableObject?.responsibleGroups || []) + .filter(Boolean) + .map((group) => ({ + label: groups.find((g) => g.id === group)?.name || group, + value: groups.find((g) => g.id === group), + })), + } + : {}; + + const showLoading = !(isNew || (lendableObject && groups)); + const title = isNew + ? 'Opprett utlånsobjekt' + : `Redigerer ${lendableObject?.title}`; + return ( + + + + {({ handleSubmit }) => ( +
+ + + + + + + + + {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} + + {!isNew && ( + + {({ openConfirmModal }) => ( + + )} + + )} + + + )} +
+
+
+ ); +}; + +export default LendableObjectEdit; diff --git a/app/routes/lending/components/LendableObjectsList.css b/app/routes/lending/components/LendableObjectsList.css new file mode 100644 index 0000000000..d12430e883 --- /dev/null +++ b/app/routes/lending/components/LendableObjectsList.css @@ -0,0 +1,47 @@ +@import url('~app/styles/variables.css'); + +.myRequestsList { + margin-bottom: var(--spacing-xl); +} + +.searchBar { + margin-bottom: var(--spacing-xl); +} + +.lendableObjectsContainer { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-auto-rows: 1fr; + gap: var(--spacing-xl); + + @media (--medium-viewport) { + grid-template-columns: repeat(2, 1fr); + } + + @media (--small-viewport) { + grid-template-columns: repeat(1, 1fr); + } +} + +.lendableObjectCard { + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0; + height: 100%; +} + +.lendableObjectImage { + object-fit: cover; + height: 100%; +} + +.lendableObjectFooter { + display: flex; + justify-content: center; + color: var(--lego-font-color); +} + +.lendingObjectsTitle { + margin-top: var(--spacing-xl); +} diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx new file mode 100644 index 0000000000..a20213dc02 --- /dev/null +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -0,0 +1,132 @@ +import { + Button, + Card, + LoadingIndicator, + Image, + Page, + LinkButton, +} from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { Link, useSearchParams } from 'react-router-dom'; +import { fetchAllLendableObjects } from 'app/actions/LendableObjectActions'; +import { fetchAllLendingRequests } from 'app/actions/LendingRequestActions'; +import abakus_icon from 'app/assets/icon-192x192.png'; +import TextInput from 'app/components/Form/TextInput'; +import { selectAllLendableObjects } from 'app/reducers/lendableObjects'; +import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import styles from './LendableObjectsList.css'; +import RequestItem from './RequestItem'; +import type { ListLendableObject } from 'app/store/models/LendableObject'; + +const LendableObject = ({ + lendableObject, +}: { + lendableObject: ListLendableObject; +}) => { + return ( + + + {`${lendableObject.title}`} +
+

{lendableObject.title}

+
+
+ + ); +}; + +export const LendableObjectsList = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const [showOldRequests, setShowOldRequests] = useState(false); + + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchAllLendableObjects', + () => dispatch(fetchAllLendableObjects()), + [], + ); + + const lendableObjects = useAppSelector(selectAllLendableObjects); + const fetching = useAppSelector((state) => state.lendableObjects.fetching); + + usePreparedEffect( + 'fetchAllLendingRequests', + () => dispatch(fetchAllLendingRequests()), + [], + ); + + const lendingRequests = useAppSelector(selectAllLendingRequests); + + const fetchingRequests = useAppSelector( + (state) => state.lendingRequests.fetching, + ); + + const title = 'Utlån'; + return ( + Admin} + > + + +

Mine forespørsler

+
+ + {lendingRequests.length === 0 ? ( +

Her var det tomt!

+ ) : ( + lendingRequests.map((request) => ( + + )) + )} +
+
+ + {lendingRequests.length !== 0 && ( + + )} + +

Utlånsobjekter

+ + setSearchParams(e.target.value && { search: e.target.value }) + } + /> + +
+ {lendableObjects + .filter((lendableObjects) => + searchParams.get('search') + ? lendableObjects.title + .toLowerCase() + .includes((searchParams.get('search') || '').toLowerCase()) + : true, + ) + .map((lendableObject) => ( + + ))} +
+
+
+ ); +}; + +export default LendableObjectsList; diff --git a/app/routes/lending/components/LendingAdmin.css b/app/routes/lending/components/LendingAdmin.css new file mode 100644 index 0000000000..27531d6973 --- /dev/null +++ b/app/routes/lending/components/LendingAdmin.css @@ -0,0 +1,43 @@ +@import url('~app/styles/variables.css'); + +.newLendableObject { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px solid var(--success-color); + color: var(--success-color); + transition: var(--easing-fast); + padding: var(--spacing-md); + + &:hover { + background-color: var(--success-color); + color: var(--color-absolute-white); + } +} + +.lendingRequestsContainer { + border-bottom: 1px solid var(--border-gray); + padding-bottom: var(--spacing-lg); +} + +.lendableObjectsContainer { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: var(--spacing-xl); +} + +.lendableObject { + display: flex; + align-items: center; + justify-content: space-between; +} + +.lendableObjectCard { + display: flex; + justify-content: center; + color: var(--lego-font-color); + background-color: var(--additive-background); + flex-direction: column; + align-items: stretch; +} diff --git a/app/routes/lending/components/LendingAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx new file mode 100644 index 0000000000..a7d57d08ac --- /dev/null +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -0,0 +1,127 @@ +import { + Button, + Card, + Flex, + LinkButton, + LoadingIndicator, + Page, +} from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { Link } from 'react-router-dom'; +import { fetchAllLendableObjects } from 'app/actions/LendableObjectActions'; +import { fetchAllLendingRequests } from 'app/actions/LendingRequestActions'; +import { selectAllLendableObjects } from 'app/reducers/lendableObjects'; +import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { LendingRequestStatus } from 'app/store/models/LendingRequest'; +import styles from './LendingAdmin.css'; +import { RequestItem } from './RequestItem'; + +const LendableObjectsAdmin = () => { + const [showOldRequests, setShowOldRequests] = useState(false); + + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchAllLendingObjectsAndRequests', + () => + Promise.allSettled([ + dispatch(fetchAllLendingRequests()), + dispatch(fetchAllLendableObjects()), + ]), + [], + ); + + const lendableObjects = useAppSelector(selectAllLendableObjects); + const fetchingObjects = useAppSelector( + (state) => state.lendableObjects.fetching, + ); + + const lendingRequests = useAppSelector((state) => + selectAllLendingRequests(state), + ); + + const fetchingRequests = useAppSelector( + (state) => state.lendingRequests.fetching, + ); + + const title = 'Admin: Utlånssystem'; + return ( + Nytt utlånsobjekt + } + > + + +

Utlånsforepørsler

+
+

Ventende utlånsforespørsler

+ + + {lendingRequests + .filter( + (request) => request.status === LendingRequestStatus.PENDING, + ) + .map((request) => ( + + ))} + + + + {showOldRequests ? ( + <> +

Tidligere utlånsforespørsler

+ + {lendingRequests + .filter( + (request) => request.status !== LendingRequestStatus.PENDING, + ) + .map((request) => ( + + ))} + + + + ) : ( + + )} +
+ +

Utlånsobjekter

+ + + +
+ {lendableObjects.map((lendableObject) => ( + + +

{lendableObject.title}

+
+ + ))} +
+
+
+
+ ); +}; + +export default LendableObjectsAdmin; diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx new file mode 100644 index 0000000000..0efbac03d9 --- /dev/null +++ b/app/routes/lending/components/LendingRequest.tsx @@ -0,0 +1,100 @@ +import { LinkButton, LoadingPage, Page } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { Helmet } from 'react-helmet-async'; +import { Link, useParams } from 'react-router-dom'; +import { fetchLendingRequest } from 'app/actions/LendingRequestActions'; +import { + ContentMain, + ContentSection, + ContentSidebar, +} from 'app/components/Content'; +import InfoList from 'app/components/InfoList'; +import { FromToTime } from 'app/components/Time'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { selectLendingRequestById } from 'app/reducers/lendingRequests'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { statusToString } from 'app/store/models/LendingRequest'; + +const LendingRequest = () => { + const { lendingRequestId } = useParams<{ lendingRequestId: string }>(); + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchRequest', + () => lendingRequestId && dispatch(fetchLendingRequest(lendingRequestId)), + [lendingRequestId], + ); + + const lendingRequest = useAppSelector((state) => + selectLendingRequestById(state, Number(lendingRequestId)), + ); + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, lendingRequest?.lendableObject), + ); + + const requestFetching = useAppSelector( + (state) => state.lendingRequests.fetching, + ); + + if (!lendingRequest || !lendableObject) { + return ; + } + + const infoItems = [ + { + key: 'Status', + value: statusToString(lendingRequest.status), + }, + { + key: 'Tidsspenn', + value: ( + + ), + }, + { + key: 'Bruker', + value: ( + + {lendingRequest.author.fullName} + + ), + }, + ]; + + const title = `Forespørsel om utlån av ${lendableObject.title}`; + return ( + + Admin + + } + skeleton={requestFetching} + > + {lendingRequest && ( + <> + + + + +
+

Kommentar:

+ {lendingRequest.message} +
+
+ + + +
+ + )} +
+ ); +}; + +export default LendingRequest; diff --git a/app/routes/lending/components/LendingRequestAdmin.css b/app/routes/lending/components/LendingRequestAdmin.css new file mode 100644 index 0000000000..0f9a283050 --- /dev/null +++ b/app/routes/lending/components/LendingRequestAdmin.css @@ -0,0 +1,5 @@ +@import url('~app/styles/variables.css'); + +.calendarContainer { + margin-top: var(--spacing-xl); +} diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx new file mode 100644 index 0000000000..ad47b7d57c --- /dev/null +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -0,0 +1,180 @@ +import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import { LoadingIndicator, Page } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import moment from 'moment-timezone'; +import { Helmet } from 'react-helmet-async'; +import { Link, useParams } from 'react-router-dom'; +import { + fetchLendingRequest, + fetchLendingRequestsForLendableObject, +} from 'app/actions/LendingRequestActions'; +import { + ContentMain, + ContentSection, + ContentSidebar, +} from 'app/components/Content'; +import InfoList from 'app/components/InfoList'; +import { FromToTime } from 'app/components/Time'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { + selectLendingRequestById, + selectLendingRequestsByLendableObjectId, +} from 'app/reducers/lendingRequests'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { + LendingRequestStatus, + statusToString, +} from 'app/store/models/LendingRequest'; +import styles from './LendingRequestAdmin.css'; + +const LendingRequestAdmin = () => { + const { lendingRequestId } = useParams<{ lendingRequestId: string }>(); + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchLendingRequest', + () => lendingRequestId && dispatch(fetchLendingRequest(lendingRequestId)), + [lendingRequestId], + ); + + const lendingRequest = useAppSelector((state) => + selectLendingRequestById(state, Number(lendingRequestId)), + ); + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, lendingRequest?.lendableObject), + ); + + const fetching = useAppSelector((state) => state.lendableObjects.fetching); + + usePreparedEffect( + 'fetchLendingRequests', + () => { + if (lendingRequest && lendingRequest.lendableObject) { + dispatch( + fetchLendingRequestsForLendableObject(lendingRequest.lendableObject), + ); + } + }, + [lendingRequest?.lendableObject], + ); + + const otherRequests = useAppSelector((state) => + selectLendingRequestsByLendableObjectId( + state, + lendingRequest?.lendableObject, + ), + ); + + const otherApprovedRequests = otherRequests.filter( + (loan) => loan.status === LendingRequestStatus.APPROVED, + ); + const otherPendingRequests = otherRequests.filter( + (loan) => loan.status === LendingRequestStatus.PENDING, + ); + + if (!lendingRequest) { + return

Ukjent forespørsel

; + } + + const requestEvent = { + id: String(lendingRequest.id), + title: lendingRequest.author?.fullName, + start: moment(lendingRequest.startDate).toDate(), + end: moment(lendingRequest.endDate).toDate(), + backgroundColor: 'var(--lego-red-color)', + borderColor: 'var(--lego-red-color)', + }; + + const otherApprovedEvents = otherApprovedRequests.map((loan) => ({ + id: String(loan.id), + title: lendingRequest.author?.fullName, + start: loan.startDate, + end: loan.endDate, + backgroundColor: 'var(--color-gray-5)', + borderColor: 'var(--color-gray-5)', + })); + + const otherPendingEvents = otherPendingRequests.map((loan) => ({ + id: String(loan.id), + title: lendingRequest?.author?.fullName, + start: loan.startDate, + end: loan.endDate, + backgroundColor: 'var(--color-red-2)', + borderColor: 'var(--color-red-2)', + })); + + const infoItems = [ + { + key: 'Status', + value: statusToString(lendingRequest.status), + }, + { + key: 'Lånetid', + value: ( + + ), + }, + { + key: 'Bruker', + value: ( + + {lendingRequest.author?.fullName} + + ), + }, + ]; + + const title = `Admin: Forespørsel om utlån av ${lendableObject?.title}`; + return ( + + + + + + +
+

Kommentar:

+ {lendingRequest.message} +
+
+ + + +
+ + + + + + +
+
+ ); +}; + +export default LendingRequestAdmin; diff --git a/app/routes/lending/components/RequestItem.css b/app/routes/lending/components/RequestItem.css new file mode 100644 index 0000000000..a2c9d95da5 --- /dev/null +++ b/app/routes/lending/components/RequestItem.css @@ -0,0 +1,52 @@ +/* same as joblistings and interestgroup, maybe own component for hoverable link container? */ +.requestItem { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + color: var(--lego-font-color); + border-radius: var(--border-radius-lg); + padding: 15px 20px; + margin: 3px 0 12px; + transition: background-color var(--easing-fast); + + &:hover { + background-color: rgba(255, 0, 0, var(--color-red-hover-alpha)); + } +} + +.requestTitle { + margin: 0 0 10px; + color: var(--lego-font-color); +} + +.statusPill { + margin: 0; + display: flex; + text-transform: uppercase; + padding: 0.375rem 0.5rem; + font-size: var(--font-size-md); + font-weight: bold; + border-radius: var(--border-radius-lg); + letter-spacing: 0.6px; + line-height: 1; + box-shadow: var(--shadow-xs); + align-items: center; + align-self: center; + gap: 0.25rem; +} + +.pending { + background-color: var(--color-orange-2); + color: var(--color-orange-7); +} + +.approved { + background-color: var(--success-color); + color: var(--color-green-7); +} + +.denied { + background-color: var(--color-red-2); + color: var(--color-red-7); +} diff --git a/app/routes/lending/components/RequestItem.tsx b/app/routes/lending/components/RequestItem.tsx new file mode 100644 index 0000000000..86c8374909 --- /dev/null +++ b/app/routes/lending/components/RequestItem.tsx @@ -0,0 +1,70 @@ +import { Flex, Icon } from '@webkom/lego-bricks'; +import cx from 'classnames'; +import moment from 'moment-timezone'; +import { Link } from 'react-router-dom'; +import { LendingRequestStatus } from 'app/store/models/LendingRequest'; +import styles from './RequestItem.css'; +import type { LendingRequest } from 'app/store/models/LendingRequest'; + +const ApprovedFlag = () => { + return ( +
+ + Godkjent! +
+ ); +}; + +const PendingFlag = () => { + return ( +
+ + Venter på svar +
+ ); +}; + +const DeniedFlag = () => { + return ( +
+ + Avslått +
+ ); +}; + +type RequestItemProps = { + request: LendingRequest; + isAdmin?: boolean; +}; + +export const RequestItem = ({ request, isAdmin }: RequestItemProps) => { + let url = `/lending/request/${request.id}`; + if (isAdmin) { + url += '/admin'; + } + return ( + + +

{request.lendableObject?.title}

+ +

{request.author?.fullName}

+

+ {moment(request.startDate).format('DD.MM.YYYY')} -{' '} + {moment(request.endDate).format('DD.MM.YYYY')} +

+
+
+ + {request.status === LendingRequestStatus.APPROVED ? ( + + ) : request.status === LendingRequestStatus.DENIED ? ( + + ) : ( + + )} + + ); +}; + +export default RequestItem; diff --git a/app/routes/lending/index.tsx b/app/routes/lending/index.tsx new file mode 100644 index 0000000000..353afeba04 --- /dev/null +++ b/app/routes/lending/index.tsx @@ -0,0 +1,35 @@ +import loadable from '@loadable/component'; +import pageNotFound from '../pageNotFound'; +import type { RouteObject } from 'react-router-dom'; + +const LendableObjectsList = loadable( + () => import('./components/LendableObjectsList'), +); +const LendableObjectEdit = loadable( + () => import('./components/LendableObjectEdit'), +); +const LendableObjectDetail = loadable( + () => import('./components/LendableObjectDetail'), +); +const LendableObjectAdminDetail = loadable( + () => import('./components/LendableObjectAdminDetail'), +); +const LendingAdmin = loadable(() => import('./components/LendingAdmin')); +const LendingRequest = loadable(() => import('./components/LendingRequest')); +const LendingRequestAdmin = loadable( + () => import('./components/LendingRequestAdmin'), +); + +const lendingRoute: RouteObject[] = [ + { index: true, Component: LendableObjectsList }, + { path: 'create', Component: LendableObjectEdit }, + { path: ':lendableObjectId', Component: LendableObjectDetail }, + { path: ':lendableObjectId/edit', Component: LendableObjectEdit }, + { path: ':lendableObjectId/admin', Component: LendableObjectAdminDetail }, + { path: 'admin', Component: LendingAdmin }, + { path: 'request/:lendingRequestId', Component: LendingRequest }, + { path: 'request/:lendingRequestId/admin', Component: LendingRequestAdmin }, + { path: '*', children: pageNotFound }, +]; + +export default lendingRoute; diff --git a/app/store/createRootReducer.ts b/app/store/createRootReducer.ts index 70d6a545d7..0d9bf0e5dd 100644 --- a/app/store/createRootReducer.ts +++ b/app/store/createRootReducer.ts @@ -20,6 +20,8 @@ import galleryPictures from 'app/reducers/galleryPictures'; import groups from 'app/reducers/groups'; import imageGalleryEntries from 'app/reducers/imageGallery'; import joblistings from 'app/reducers/joblistings'; +import lendableObjects from 'app/reducers/lendableObjects'; +import lendingRequests from 'app/reducers/lendingRequests'; import meetingInvitations from 'app/reducers/meetingInvitations'; import meetings from 'app/reducers/meetings'; import memberships from 'app/reducers/memberships'; @@ -68,6 +70,8 @@ const createRootReducer = () => { groups, imageGalleryEntries, joblistings, + lendableObjects, + lendingRequests, meetingInvitations, meetings, memberships, diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts new file mode 100644 index 0000000000..2d5e09201f --- /dev/null +++ b/app/store/models/LendableObject.d.ts @@ -0,0 +1,38 @@ +import type { EntityId } from '@reduxjs/toolkit'; +import type { PublicGroup } from 'app/store/models/Group'; +import type { RoleType } from 'app/utils/constants'; +import type { Duration } from 'moment-timezone'; + +interface LendableObject { + id: EntityId; + image: string; + title: string; + description: string; + location: string; + hasContract: boolean; + maxLendingPeriod: null | string | Duration; + responsibleRoles: RoleType[]; + responsibleGroups: EntityId[]; +} + +export type ListLendableObject = Pick; +export type DetailedLendableObject = ListLendableObject & + Pick< + LendableObject, + | 'description' + | 'location' + | 'hasContract' + | 'maxLendingPeriod' + | 'responsibleRoles' + | 'responsibleGroups' + >; + +export type UnknownLendableObject = ListLendableObject | DetailedLendableObject; + +export type EditingLendableObject = Omit< + DetailedLendableObject, + 'responsibleRoles' | 'responsibleGroups' +> & { + responsibleRoles: { label: string; value: RoleType }[]; + responsibleGroups: PublicGroup[]; +}; diff --git a/app/store/models/LendingRequest.ts b/app/store/models/LendingRequest.ts new file mode 100644 index 0000000000..6ae0cc5bbf --- /dev/null +++ b/app/store/models/LendingRequest.ts @@ -0,0 +1,30 @@ +import type { EntityId } from '@reduxjs/toolkit'; +import type { Dateish } from 'app/models'; +import type { PublicUser } from 'app/store/models/User'; + +export enum LendingRequestStatus { + PENDING = 'PENDING', + APPROVED = 'APPROVED', + DENIED = 'DENIED', +} + +export function statusToString(status: LendingRequestStatus): string { + switch (status) { + case LendingRequestStatus.PENDING: + return 'Venter på svar'; + case LendingRequestStatus.APPROVED: + return 'Godkjent'; + case LendingRequestStatus.DENIED: + return 'Avslått'; + } +} + +export type LendingRequest = { + id: number; + author: PublicUser; + startDate: Dateish; + endDate: Dateish; + message: string; + status: LendingRequestStatus; + lendableObject: EntityId; +}; diff --git a/app/store/models/entities.ts b/app/store/models/entities.ts index c63d0fc1f9..a4d453ac99 100644 --- a/app/store/models/entities.ts +++ b/app/store/models/entities.ts @@ -1,3 +1,4 @@ +import type { LendingRequest } from './LendingRequest'; import type OAuth2Grant from './OAuth2Grant'; import type { EntityId } from '@reduxjs/toolkit'; import type { UnknownAnnouncement } from 'app/store/models/Announcement'; @@ -18,6 +19,7 @@ import type { UnknownGalleryPicture } from 'app/store/models/GalleryPicture'; import type { UnknownGroup } from 'app/store/models/Group'; import type { ImageGalleryEntry } from 'app/store/models/ImageGalleryEntry'; import type { UnknownJoblisting } from 'app/store/models/Joblisting'; +import type { UnknownLendableObject } from 'app/store/models/LendableObject'; import type { UnknownMeeting } from 'app/store/models/Meeting'; import type { MeetingInvitation } from 'app/store/models/MeetingInvitation'; import type Membership from 'app/store/models/Membership'; @@ -54,6 +56,8 @@ export enum EntityType { Groups = 'groups', ImageGalleryEntries = 'imageGalleryEntries', Joblistings = 'joblistings', + LendableObjects = 'lendableObjects', + LendingRequests = 'lendingRequests', MeetingInvitations = 'meetingInvitations', Meetings = 'meetings', Memberships = 'memberships', @@ -94,6 +98,8 @@ export default interface Entities { [EntityType.Groups]: Record; [EntityType.Joblistings]: Record; [EntityType.ImageGalleryEntries]: Record; + [EntityType.LendableObjects]: Record; + [EntityType.LendingRequests]: Record; [EntityType.MeetingInvitations]: Record; [EntityType.Meetings]: Record; [EntityType.Memberships]: Record; diff --git a/app/styles/globals.css b/app/styles/globals.css index 8ff2026e00..4e7de3b884 100644 --- a/app/styles/globals.css +++ b/app/styles/globals.css @@ -98,8 +98,6 @@ thead { th, td { padding: var(--spacing-sm); - width: 140px; - display: table-cell; } th:first-child { diff --git a/package.json b/package.json index 957a3dff24..ec46c7420f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,11 @@ "packages/*" ], "dependencies": { + "@fullcalendar/core": "^6.1.9", + "@fullcalendar/daygrid": "^6.1.9", + "@fullcalendar/interaction": "^6.1.9", + "@fullcalendar/react": "^6.1.9", + "@fullcalendar/timegrid": "^6.1.9", "@loadable/component": "^5.16.4", "@reduxjs/toolkit": "^2.2.4", "@sentry/browser": "^7.105.0", diff --git a/packages/lego-bricks/package.json b/packages/lego-bricks/package.json index 6c09b9e9bb..7b8be3fe9a 100644 --- a/packages/lego-bricks/package.json +++ b/packages/lego-bricks/package.json @@ -1,6 +1,6 @@ { "name": "@webkom/lego-bricks", - "version": "1.2.1", + "version": "1.2.2", "description": "Component library for lego and other Abakus projects", "author": "webkom", "license": "MIT", diff --git a/packages/lego-bricks/src/global.css b/packages/lego-bricks/src/global.css index f05510ec27..fcfd30bd26 100644 --- a/packages/lego-bricks/src/global.css +++ b/packages/lego-bricks/src/global.css @@ -93,7 +93,6 @@ thead { th, td { padding: var(--spacing-sm); - width: 140px; display: table-cell; } diff --git a/yarn.lock b/yarn.lock index 1c27cdc75d..f806860b21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2211,6 +2211,35 @@ dependencies: tslib "^2.4.0" +"@fullcalendar/core@^6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.9.tgz#ea735b0dd0a0a487969ebbb6c99b0967e07568c0" + integrity sha512-eeG+z9BWerdsU9Ac6j16rpYpPnE0wxtnEHiHrh/u/ADbGTR3hCOjCD9PxQOfhOTHbWOVs7JQunGcksSPu5WZBQ== + dependencies: + preact "~10.12.1" + +"@fullcalendar/daygrid@^6.1.9", "@fullcalendar/daygrid@~6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.9.tgz#efb8aabb2f928ac0b05a77c5443accb546ae5818" + integrity sha512-o/6joH/7lmVHXAkbaa/tUbzWYnGp/LgfdiFyYPkqQbjKEeivNZWF1WhHqFbhx0zbFONSHtrvkjY2bjr+Ef2quQ== + +"@fullcalendar/interaction@^6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-6.1.9.tgz#9023922df24c296cb7f4671887f1731f5d5a5db2" + integrity sha512-I3FGnv0kKZpIwujg3HllbKrciNjTqeTYy3oJG226oAn7lV6wnrrDYMmuGmA0jPJAGN46HKrQqKN7ItxQRDec4Q== + +"@fullcalendar/react@^6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-6.1.9.tgz#280fd543901d792c19b50f363c55cc3068917299" + integrity sha512-ioxu0V++pYz2u/N1LL1V8DkMyiKGRun0gMAll2tQz3Kzi3r9pTwncGKRb1zO8h0e+TrInU08ywk/l5lBwp7eog== + +"@fullcalendar/timegrid@^6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@fullcalendar/timegrid/-/timegrid-6.1.9.tgz#83b61ac734638d11182aeb579b638fa0bc52ea32" + integrity sha512-le7UV05wVE1Trdr054kgJXTwa+A1pEI8nlCBnPWdcyrL+dTLoPvQ4AWEVCnV7So+4zRYaCqnqGXfCJsj0RQa0g== + dependencies: + "@fullcalendar/daygrid" "~6.1.9" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -12531,6 +12560,11 @@ postcss@^8.0.0, postcss@^8.4.28, postcss@^8.4.33, postcss@^8.4.35, postcss@^8.4. picocolors "^1.0.0" source-map-js "^1.2.0" +preact@~10.12.1: + version "10.12.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21" + integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg== + precond@0.2: version "0.2.3" resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"