From 51f8221265a386982d98b786858b62dfe5608d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eik=20Hvattum=20R=C3=B8geberg?= Date: Wed, 4 Oct 2023 21:51:25 +0200 Subject: [PATCH 01/44] WIP --- app/components/Search/utils.tsx | 6 + app/routes/lending/LendableObjectDetail.css | 0 app/routes/lending/LendableObjectDetail.tsx | 59 +++++++++ app/routes/lending/LendableObjectsList.css | 43 +++++++ app/routes/lending/LendableObjectsList.tsx | 129 ++++++++++++++++++++ app/routes/lending/index.tsx | 26 ++++ app/store/models/LendableObject.d.ts | 15 +++ app/styles/globals.css | 2 - package.json | 5 + yarn.lock | 34 ++++++ 10 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 app/routes/lending/LendableObjectDetail.css create mode 100644 app/routes/lending/LendableObjectDetail.tsx create mode 100644 app/routes/lending/LendableObjectsList.css create mode 100644 app/routes/lending/LendableObjectsList.tsx create mode 100644 app/routes/lending/index.tsx create mode 100644 app/store/models/LendableObject.d.ts diff --git a/app/components/Search/utils.tsx b/app/components/Search/utils.tsx index 1b08086491..f8ffe86777 100644 --- a/app/components/Search/utils.tsx +++ b/app/components/Search/utils.tsx @@ -99,6 +99,12 @@ const LINKS: Array = [ icon: 'stats-chart-outline', url: '/polls', }, + { + key: 'lending', + title: 'Utlån', + icon: 'cart-outline', + url: '/lending', + }, { admin: true, key: 'announcements', diff --git a/app/routes/lending/LendableObjectDetail.css b/app/routes/lending/LendableObjectDetail.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/LendableObjectDetail.tsx new file mode 100644 index 0000000000..518bcfe7e5 --- /dev/null +++ b/app/routes/lending/LendableObjectDetail.tsx @@ -0,0 +1,59 @@ +import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import React from 'react'; +import { Helmet } from 'react-helmet-async'; +import { useParams } from 'react-router-dom'; +import { Line } from 'recharts'; +import { Content } from 'app/components/Content'; +import NavigationTab from 'app/components/NavigationTab'; +import type { DetailedLendableObject } from 'app/store/models/LendableObject'; + +type Params = { + lendableObjectId: string; +}; + +const LendableObjectDetail = () => { + const { lendableObjectId } = useParams(); + const lendableObject: DetailedLendableObject = { + id: lendableObjectId, + title: 'Soundbox', + description: 'En soundbox som kan brukes til å spille av lyder', + image: + 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', + }; + + return ( + + + +

{lendableObject.description}

+ { + console.log(info); + }} + /> +
+ ); +}; + +export default LendableObjectDetail; diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/LendableObjectsList.css new file mode 100644 index 0000000000..f6ca193daf --- /dev/null +++ b/app/routes/lending/LendableObjectsList.css @@ -0,0 +1,43 @@ +@import url('~app/styles/variables.css'); + +.searchBar { + margin-bottom: 2rem; +} + +.lendableObjectsContainer { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 2rem; +} + +.lendableObjectCard { + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0; +} + +.lendableObjectImage { + height: 15rem; + object-fit: cover; +} + +.lendableObjectFooter { + display: flex; + justify-content: center; + color: var(--lego-font-color); + font-size: 1.1rem; + font-weight: bold; + background-color: var(--color-gray-1); +} + +@media (--medium-viewport) { + .lendableObjectsContainer { + grid-template-columns: repeat(2, 1fr); + } +} +@media (--small-viewport) { + .lendableObjectsContainer { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx new file mode 100644 index 0000000000..58fb301f4c --- /dev/null +++ b/app/routes/lending/LendableObjectsList.tsx @@ -0,0 +1,129 @@ +import { usePreparedEffect } from '@webkom/react-prepare'; +import qs from 'qs'; +import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { Link, useHistory, useLocation } from 'react-router-dom'; +import Card from 'app/components/Card'; +import { Content } from 'app/components/Content'; +import TextInput from 'app/components/Form/TextInput'; +import { Image } from 'app/components/Image'; +import NavigationTab from 'app/components/NavigationTab'; +import type { ListLendableObject } from 'app/store/models/LendableObject'; +import styles from './LendableObjectsList.css'; + +const LendableObject = ({ + lendableObject, +}: { + lendableObject: ListLendableObject; +}) => { + return ( + + + {`${lendableObject.title}`} +
+

{lendableObject.title}

+
+
+ + ); +}; + +type Query = { + title?: string; +}; + +const parseQuery = (search: string): Query => { + const { title } = qs.parse(search, { + ignoreQueryPrefix: true, + }); + + return { + title: typeof title === 'string' ? title : undefined, + }; +}; + +const LendableObjectsList = () => { + const location = useLocation(); + const query = parseQuery(location.search); + const history = useHistory(); + + usePreparedEffect( + 'fetchLendableObjects', + () => { + console.log(query.title); + }, + [query.title] + ); + + const lendableObjects: Array = [ + { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 2, + title: 'Noe annet enn grill', + image: + 'data:image/jpeg;base64,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', + }, + { + id: 3, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 4, + title: 'Noe annet enn grill', + image: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAQEBUQEBAVDxAQEBAVDw8PFhAPEBUVFRYWFxUWFRUYHSgiGBolHRUVIjEhJSkrLi4uGCA0OTQtOCgtLisBCgoKDg0OGhAQGi0lHyUwLS4tLS0tLSsuLi0vLi0tLS8rLS0vLS0wLS0tLS0tLS0rKy8tLy0tKy0wLS0tLS0tLf/AABEIAOEA4QMBIgACEQEDEQH/xAAbAAACAgMBAAAAAAAAAAAAAAAAAQIFAwQGB//EAFEQAAEDAgIFBwcHBwgKAwAAAAEAAgMEERIhBQYxQVETIlJhcYGRBxQVMjOhsSNCU3LB0dJUdIKSlLTwJGJzk6Kys+EWNENVY6O10+LxFyU2/8QAHAEAAgIDAQEAAAAAAAAAAAAAAAECAwQFBwYI/8QAQhEAAQMCAwQFCgMHAwUBAAAAAQACEQMEEiExBUFRYQZxgZGhExQiMkJSwdHh8BaSsRUjM1NyovFUYrI0gpPC0gf/2gAMAwEAAhEDEQA/APSSokqRVHT6Np5Zqh0sEUzhPGA6VkchA5CA2uRszPiuTgAzP3mtyrrEOKWIcVVxaK0e6+Gmpzhc5rvkYcnt2jZuSk0Zo9tsVPSi7sAvHAOda+HZtsE8GcZ9yJP3/hWtxxRiHFVfoigz/k1LkQD8lBkTsBy2qA0Zo/Di81p7GTAL08YOLFgtbDfb9+zNGEc+76on7+wrfEOKd1Vu0PQC96amFjY3igFjtzyyWCq0XTRT0zo6eKJ3nThiijjjdbkKjK4CIH2PqiT9/wCFdISTUFJCEJITWvW1rIWhz3YQTYWBLiexaH+ktLxl7cMf41z2sNfy0lgeYwYW8CATzu8+6yql1jZfQezNqx17iNQiSA6A2d2Q1A9aSc5iBruKOzmFgL5n75LthrHS8Xj9H/yWeg01BOSyJ+JwFyCC3LZ9q8/kduWGjqXU87ZW7ngkbjnZzT2gnxWTcdAdnvpPFuXtfBwy4FuLdi9GY3ZHLVKrs9gBwEzzj5L1VCxU07ZGNew3a9rS09RWRciexzHFrhBGRB1BGoPUtShCSFBCdt/uWJ38XWQnIdv2LE8rqXRuxZQsmVAPSeMRPI6DqAzjiSVz7b126tduZPosyA5xmfh2dawyquqFuylV9Q5eopBaZaTpC0hzTYg3Bauko6nlWB+ziOBH8e9ctM5WurrvaDjY/Z9y0XS7ZzK+zzXj06cEHfhkAjqzxdYy1K3nR67dSuxS9l8gjgQCQfCOo5zAV4khC5GugIQldCELOVX6N9rU/nLP3eBWBVLBQQyzVDpI2vInYAXbbchAbe8qTYOIH5+0OY/VUncs/oNlrB8ov7TnBxcSCHE4gbYri9rbBayyN0S0EkOeCZjKPUs1xxXwjDaxxu3XzvtzWGHR1E+4ZHE8tNnBuZBuRY8MwfBTfoelAuYYwLgXPEmw95Ctc+cnOd2gdfvSo4eACKnQkL4zHz2B0kz3GLA1xMpkLgTbMXmeVkm0biBAkkaDK2UAckcLg/HldpyLsze+zKyh6FpvoGeCPQ1N9DH4KGNsRiP5R/8ASeHl99yxv0HGXPcXPdyhuQ7CQMphll/x3eA756RFn0vVVH93qE/Q1N9DH4LUqtHQxy0zmRsY7zlwuNtvN50y5rtXE9g4f1JxG5XSEklSppqp1jr+SiLWmz5LtvwaQQ4+Nh3q1WhpqiE0Rba7mjG3rIBu3vHvstvsGpb09o0X3PqBwngD7JPIOgnlO6Vdblgqtx6T9/XkuHCxyGyZFjY7likN13+DMFemdkoKE0eIW8O1NRkfhF1YAZyVRK6jUbSdw6ndtbdzfcC37fFdauc1M0ZycXLOHPm2A7Qw2N+859gC6NcM6Yvtn7XrG30yDuBePWI7cjxcHHetDcFpqktTQkheZVCi85DxPgsD3LORcW4rQnfbI7etdR6MXzLizbS9umII5eyRyiAeBHMLn+37N1G6NX2X5g89475I4zlMFQmeqypetmaRaMpXr6bVpCtdyudXo7BztzrBv2/AKsgp3PcGtFyfBq6anhEbA0bB8TtK8z0w2myhZG1B9OpGXBsgknrjCOMkjQrfdHbJ1W5Fc+qyc+LiCI7AZPZxWZJCS5MvfJoUUJwmtoqu0d7Wp/OI/wDAgW+VTQ0LZJqhznytInYLRTTxD2EBzDHAE57Umx6U/eYVJ3J1WgWyAh0hN3XAAwi15jY2Nz7d5vfa1ptlnmfonN5DwDJLE/nNDrGN2IG97k7B2DiSSm6JiN7TTmxsbVVUbHgeftTOh4/pZ/2qr/GrTVdvd4dR+CWHksDdCYpJXS8m5sjwb2xOIHKYQ4EWsA/rzCtmNsAL3sALnabKv9Dx/Sz/ALTV/jR6Hj+ln/aav8ai95fEnw+qYEKzVdpL2lN+dH/AqFH0PH9LP+01f41rVFA2OWmcHyuPnThaWaeVv+rz/Nc4hJoE6+HLrTJKukJIVanCaV0IshC47Wah5OQvaObJd3UHkm4+3v6lQWPAr06SJrhZ7WvHB4a8e9YjRRb4Yf6uL7l0rZnTynb2tOlc0nOe0QXAjOMgc98a8TnvgbKntCGBrhJH3wXmpB4LY0Lo81NQ1puI4+fIf5oIuO07O9d+7R0P5PH/AFYCyQ07GCzGNjH/AA2tb42WTd//AKDSNB4tqThUIIBdhhpO/IkkjUDjySq38thoWRrQBYCwAsANgAUklFzwASTYAEkncB6y5eAXGBr9/fPrWuTumvNNbdL1D3Nmie+MRO5gYS2zTsJtv2X7V0Wp2tLatvJS2bUMGYGTXgbXNHS4jvHV67aPQu+srFt4SHQP3jQM6fjDgPaI0PFvpKllw1zsPdzXULHIxrhzhfjx7lNC8rRrVKLxUpuIcNCDB71ZUpsqNLHgEHUESFXS6LDtjy0cCM/G6gzRA+c4uHRaMP2lWiit4OlW1sOHy3bhZPfh8dea1f7A2fixeS/udHdijs0UIoWMFmiw37ye071lUULR1ar6rzUqElx1JMk9ZOa2tOm2m0NYIA0AyA6kIRdCrU0IQhCFsOVfo4/K1H5wz93gW8VpP0cC9z2yyxmQguEZjwkhobfNp3MHghsZyfuVUtQ6IkwNY2UR8ngGOIGOR7WhwBe7ebuvbZcHjlkk0U44ryl2KVsjQ50tgRNI+2Tshhc1uXRCzeYO/KZ/GH8CXmDvymbxh/ArfKnj4JYRw8VjqNGPc1wbPI17pXPLg6S2G7y1gBJwgB42D5qsWCwAJuQALnaetaXmDvymbxh/Aj0e78pm8YfwKDnSIJ8EwIW8tDSPtKb86P8AgVCfmDvymbxh/AmzRvPY90ssnJuL2iQx4blrm3yaNzikA3j4Jkngt1MNUmtUyQ3b4KolBKQYkSB/ksckvHIcFhMnBMNJQGlZzJ1JF6wZp2TwhSwhZcaV1GyVk4ThTVZrHIW0z7byxncSL+4FWK1NKU3LRPjG0tJb9cG494Wx2PVp0b+hVq+o17CeQDgZPIankovBLSAuAnaHNLTmCCCFyzuUppg5ji17HBzHjI5bCulkdbI5EZEHIgjaCq7ScQkb1jYV9GUDBIOh13/fxWpK9J1Z002sgEgsHjmzNG528j+ado8NytV49qdpc0lULm0UnMlG6xI53dtvw7V7AHLh/TDYI2TfRSH7qpLmcveb/wBpIj/aW6mVsrepjbnqE00kLyayEIUUIQhCSE04UroUU0IhbBSQUKB1VYQhJCSE0kkITTCyMaoNCzXsL+CiSoOKHuw9q1JZbdZRPLbrJ2LXY3eVYxsZlSa2FIAnMrKGoaFNMlSRZCEkkJpISuhCEIQmhUWndXWzkyRuEcp2k5Ru+tbMHrHguPq9WK69mxh3WHst7yD7l6Whes2Z0z2nYURRbge0ZDGCSBwBDmmBuBmBkMoAofbMeZXmujtQah5xTPZCL85rTykncBl716TG3C0NuTha0XO04Ra5UrqKwdtdI77a5b5yWw2Ya0QBOusuPDNxHJTpUWs0TSQmtErYSQkhCakooTTTSQhCELZKSZSVZ1VQSTUXEAXJsBtJyAXlGlNO1umKk01C4x0wvzgXRBzAbcpM8Zhp3MHHYd2ZZ2T7kuMhrWiXOOgHzO4b1CrVDIyknQL1fENm/gmvK3eSqZgxx1jDMLkDk3xZ/wBIHEjtwrb1N1pqaeq9HaQJJxBkckhvIx5tha53z2uuLOzNyNoOWU/ZlN1Nz7asKmHMjCWmOIBmY3/MgKvy5BAe2J5yvTowsc0oJyIIbwzWQGwJ6l5F5HgA+qAFrsptmW+VYdtaCtRq1sUYMOUa4jGs5R1GeSliio1saz4BemesbrO0KEbVmCoJV6SE0lFCCUl5f5RNBV89aJIonzxcnGIDGRaNw9YbRgOLPFltGeWXo2jI5GwRNmdjmbFGJXj5zw0B57zdZte0bSo06gqBxcMwNW6ZHPnnkM9JVTKhc5zYiN/FbaSSFhq5CEJIThCEITQhAKSROaE00IXnmkdJzu09FTmR3IRyMwRDKO5py4kgesbnfs3LKtbV1wXAGMLS49Q4c1VUqBgHMgd69DQhJYytTukhJCE0JIQhbRQgoUCqwuc8oNWYdGzuGRewRi2R+Vc1h9ziqnyS0TWUTpvnTzPud+GPmBvZcPP6S3PKgwnRktvmvpyezlWD7VHyXOvoyMdGWoB75HO+Dgtw3LZJI31QD1YZHisY/wDUCfd+MLrV5h5YaPC+nqWc17myRlwyN2EOjN+Iu9enrzryySARU7d5lld3NaAf7wUdhOIv6cb5H9pTuh+6K9AoarlaeKX6aKKT9dod9q8R1Q07LSGVsEXLVFTyLIW2LgC0vJJaM3esMu0k5L2PV6MsoKVh2tpacHtEbbrznyQ0bXTTzEXdEyJrDw5QvxHwYB3lX7LdSpW10XtlowZaT6ToBjONJ5KFQOLqYBzzzW1JDrKByuPr5FvmhI6sOGx8SVbak67GreaWqaIqkYsBaDG1+H1mlpza8WNx1HZay7QLyvXyEU2l6eoj5pkNO91t7my4XnvbhHipWtSntAuoPpMa6CWljcOYzg55g/eZBDqB1GHhxInOTK9VXD6e9O+cyeaYfNrt5G/md7YG39fP1sW1duULUW1x5J2PA10iIeMQ64kZ81kvZiESR1GF5Hp7WLTlFhFRK2Mva8ssylffDa/qg29YL1peX+Wj16b+jqvjEvUFsNolj7e3qtY1pd5ScLQ0ZOAHhxPHiqaIIe9skxGpncVGSQNBc4hrWglzjkABmSTwXnFTrjpCvmdDoyO0bf8AalrC+17B7jJzYwc7Ai+XcOh8pdS5mjpA025R0cZP81zgXDvAI71xGqut76Gn5KOh5XFI575sb2F5OzIRnYABt3daydmWLnW7rhtMVHThAdGEaS4gkA6xHb1Qr1YeGF0DXLXsVjV1esFE0zSuE8TRd9xDLG0cXBga8DrGQXZap6xx18ONowSMIE0V74SdhB3tOdj1EblyR8pU/wDu2/Vyr/8AtLQ8mr3t0i/DG6KKWKf5OzsLQHNcxtyBfCLgdpWRcWNV9tUqV6LWOZmC3CA4bwQCc94O/TkYMqtbUaGuJBygzlzXq0sga0ucQ1rQS5xyAAzJJ4Lzap1w0hXzOh0XHhjb/tC1hfa9g9xk5sYOdm2vl3DofKZUuj0c8NNuVfHGSOiTdw7w0jvU/JzRNi0fG4DnTYpJHbySSG37Gho7lr7VlKham6ewPcXYWh2bRlJJG/qOUq6oXPqeTBgRJjVcxVVun6FvLTEVELc5LiGRjRvxYA1wHXsG9dnq5p+OugEzBgc12GWIm5Y62y+8HIg/A3CusIORFwciDmCOtVuj9DU1Iwtp4WxA4cRaLudY5YnHM2ubXOV1TWu6VwyDTa18iCwQCN+ITrpBHbpnNlJzHZOkc/gVTa3P0sJI/R7bx8meV/1b1r5e1N9nBedyu0j6SBcP/scTcI/k+3ksth5P1P4uva4yvNK3/wDSN/pI/wB1Ww2Tdw2pT8mz0abzOHMxGTjObTOYy3Km6pZtdiObhvyHVzWXldZeh/038S6/VU1pgPn4tPyjrD5H1LDD7PLj1q5SWrr3vlmYfJU275azCe+Tl9FkMo4TOInrMhCSaSw1chCaE01tFJMqKqOqqWlpqgFTTy07jYSxOZi4EjJ3cbHuXmuoGnho+WWirPkQZLlzvVjlADXBx3NcA2ztmV9huvV1QayapUtfzpQWSgWbPFZsluDrghw7Rlc2stnY3VNtN9vXBLHQZGrXDQie47437jRVpuJD2ajxVnNpOnYzlHzxtjtfGXsDbdt15ZpmpOm9JRwwg+bsGEONweSuDNKejfIAdTdl8rdnkniDrmqfh4Nija/9YuI9y7TQOgaeijwQMtitjkccUjyNhc7xyFgL5BZVKtZ2U1Ld5fUghpw4Q2dTnqeG7vUHNqVYDxA35zKtHgAADIAWAXi/k207HSVL2TODIqhrWmR2TWyMJLC7g3nuF91wvaCclxNB5OaSNkkcj3ziUMs52FkkZZis5hGw849R3qjZtxb0qFalWmHYIjXInPhIkETropVWPc5pZun4fquwfOxrOUc9rWAXMhIEduOPZZeU19QNK6ZiEN3QxGIB24xxOL5H23Ak4R2t4qz/APieHFcVTsF98UZf+te1+5dhq9q5TULC2BpLnW5SWQh0j7bLnYB1AAK+lVtLMOfRqGpUIIb6JaGzvMnM9XxlJzalQgOECZOcyrcoQhaRZa8t8s3r039HU/GJeoLyryoztqa2CljOJ7G4HWzs+dzQG9oDWn9IL1Vba+EWVqDwqHsLgQe1Y9LOrUPV+iotd9FuqqGWKMXkAa+No2lzCHYR1kAjvVB5MdYY3U4o5Hhk0RdyQccONjiXDDfeCSLcADxt3a5TWHUKkq3mUF1PI65eYg0xvJ3uYd/WCL77qNrcUH0HWtxIaTiDgJwuiDI3gjh8ZDqMeHiozXQjiF0OktJQ0zDJPIImDe45nqYNrj1BUep+tL9IOm/k5jiid8nLive55rHDc+1ibEjPsvSUfkrga68lQ94FubGxkRI4Ekuy7LLuNH0MVPG2KFgjjZ6rW+8k7STvJzKhVp2NKmW0yajzHpYSwNHIak7s/wDLaaznAuyHCZlVeu+jHVVDLHGLyDC+No2ksIdhHWRcd6ovJlrBE6nFJI8MmiLuTDjhxscS4Yb7SLkW22AXcrk9YNQaWreZWl1PI4kv5INdG8na5zDv7CL77qy0uaLqDra4JDZxNcBOF2mY3iNw+MhVKbg8VGa6Ecfquh0tpWGkjMs7wxoBsMuUceixu8lU+q+sbq+B8roDBhkDb4sbHHacJsDllfLeqOh8ldO12KWokkaNrYmMhuBuLruNuyy7LzeOGNkUTRGxuTWNyAA/9qNQWbKeCk4veSPSILQANwBOZO+ZjdGSkw1XOlwgcNfFbERXmenZW0+sDJpThjJhdiOwNdHyVz1BwNz1L0mJVWsurUFe1olxMey+CWKwkAO0G4sW9XhZGz7hlCs7yk4XNLTGoB3juTuKZe0YdQZHYrWrrIomGWWRscYFy95Ab3HesejK+OpibNE7FHICWnYciQQRuIIIsuHp/JZCHXfUuc0fRxxxu7MRLvgu50bo+KmibDCwMjZsaLnbmSScyTxKhcUrVjIpVC90+7hAHbnMxyQx1Qn0mwOuVsITSWEr0IQhCFtFRTckqzqqghCEJJpqKEJoTCi5SSKEJBNJNNNCpdboKuSlc2ieWT4mWwljC5t7Obid6uRvcEHm7VcpKylU8m8PABggwRIMcRwSc3ECFxWpmo/mr/OapwlqMyxrbujYXXu8uOb3m5z3XO3au0QhWXN1VuanlKpk/DgBuH3qlTptptwtQhCFjqaSEITQhAQpsCEJPyCr533fbc3L71s1c+EF3c0de5aFOFbTbAlMDJbkazLHGFNQKaE0kIQhCEIQhCihOELcKipFRVR1VaEkIQhCEE2zP3rTdpKAfPv2An3rNtdm3l0JoUnPHENJHfEeKxq15b0P4tRretwB7tVtoUGPDmhw2EXF9qyLFqU3U3FjhBBII4EZFZDXBwDm6HMKKaaioqQQhCSE0IQhCEJIQmhCEAKYbxSlEoa1RleO4bSlJJlwG8qqq6kv5rfV3nj/AJKbGFxQBKhPPyjsvVHq/etmBqwwRLejarnkAQFIqTQpJhJUoQhJ7w0XcQ0DaXENb+sVqS6ThblygP1SX/AFZNvZXNz/AAKbn/0tc7/iCpNY53qiVuKK0BpmAm2PD1vDme+1lvRyNcMTSHNOxzSHNPeFK5sLq1jzik9k6YmubPVIEoc1zfWEJoQhYyS3CkhyiqTqqkIQnZLEOKYaToFCX2b+z7AuVC6uQfJv7PsC5MLrnQvPZg/qd+pXPOk4Ivs/db8V0lB7Jn1Stla2j/Zs7D/eW7Shpe0O9XfuXNtosxbRrNnWo8Z6eudV7iyMWlI/7G/8QsKLrpmCEbGN7eaSswlYNjbdgC3DOi9Q5mqOxpPxCrO0Gj2fFcmATsF+xTETuiV1nnDePxS5dvH4q0dF2+1VP5QPiVH9ocG+P0XK+av6Dv1Sn5o/oO8CuoM7el8UuXb0gp/hml/Md3BLz8+6FzHmknQd4FHmzug79UrpfOGdIKJqWdIe9H4Zo/zHdwS8/d7q5t0bx813eFrVEuHaCTwsf4C6s1cfS+P3JOr4+l7nfcl+GKe6oe4fMKQv49nxXBzSOftyHRClHEu1fpGPfc933rUl0jFvjB+sGBB6NvjKp/bH/sVMbSb7vj9FzzGLO0LNUzxvPMY1lhnhJP8AksS83eWxtqpplwMbxp/nis2lU8o3FEIVdpXSQgFvWkcMmnMWzzeO7ZvW5UShjXOOxrXE925cRVzukeXuzJJPju7Ny9N0T2AzaFV1auJpMyj3naweQGbuMgcVn2lAVHS7QKVVWSSHE5xvuBJIHYNgWFRc63arqh0A5zcUp5MkZMtzxwLgdnYupXl/abNoNdXcGM0aI8GtaJy5CBvhbR9VlIZ5ffAKnTp6iWF2KJxHFlzgd1EKz0hoV8YxNONo2kA3HaOHWqtTt7q1v6BdSc2pTdkd4PJzSPBwnMFMFlVuWYVl/pdJ9Az/AJn4kKtQsD8NbG/0re93zVHmVLh+q9KMaXJrKVFcEJzXn5K1K6YxMDtoG7vA296qXaUjOZYfEKy057Hw/vNXMkZd4XaeitFh2dQdEEjUZHU8FzbpBVf55VBMgRAOY9UHRdY2kbbJ1rtv4quZoLjJ/Zt7yVcN3djULntXpZtUOc1tQDMicLScusHwhevZ0e2eIPk/7nfBywxU+EAA3HHfv+9ZYm84dqaWO2dic921eefVfVqmo8ySZJ4kmSe0rctYGMDG5ACAOQWeRYi62zLtsk+rj6VvrAt+KxmoYdj2n9Jqk4QZaO5AnesnLO6RS84f03eJWPEDsIPYghPzqs3R7vzH5p4W8Apmpf03eJUTUv6R8VAqJUheXH8x35nfNGBnAdwUjUP6R8VAzv6R8SolQKmLu4/mO/M75pimz3R3BSdM7pHxUHTP6R77lIrG4qXnVY61HfmPzUsDeASc88fgsLlJ8jR84eIWB1QzpjuIKiZd62fipgRot2hGZ7AtrCqN1c5p+TBdfbkbLZgrpTtYovou1TzUtYThpyd7pGt7rOP2BcgF1GnXF0GYt8o0/wBlwPxXLrrvQkAbKEa43z15a9kdkLcWH8HtPw+i2tXaYS1V3ZthaXAbiWltveR4LtcK4rVqoEVUWnITNIB63FpHvbbvXbryXT7yv7Rp4vV8mMPDV2LtnXfpO5YN5PlTKg5i4zS1PycpaPVvdvYcx4Xt3Ltlx2n5Q6Z1tjLNv9S4PvuregL6nnlZg9Qsk9YcA3tgujtVuzycZG6FXoQhdVW1Xo89SGbbDPIk24LXFYCbB8ZJNgOaVq6x+r+l9hVXoz20f1h8V4LZewrB2zqdc0mlxYHGRik4ZOs6nguLXm07zz19FtUgYy0RAgYoGgGg49qvdJRPdE4AXPNsARxHYqX0VOdsZHbhb8V010l5q36a1rekGW9uxoGklxA7Bh/Vbur0Zo1nl1aq9xOp9EE+B/RJuwDqCaV0XXjXOxEk716QCE0kXRdRTTKxmNp2gKd1G6aawupYztYPAKPmMXQHgFsXSupBx4okrX8wj6IS8wj6K2rpXTxu4okrW9Hx8FH0dH0Vt3RdPG7inJWn6Ni6A8EDR0XQHgFtITxu4okrXFFH0R4BSFOwbgsqEsR4qUlQETeCeAcFJJJJYaunD2OacsTSAeBOw9xsuHnjLHFrhYtJFuBC79VGmtFCYY2e0AzbsxgX8XXK9t0O27Tsarra4MU3kEE6NdEZ8A4ASdxA3SVnWVcUyWu0P6rkJYwc9hbsI2hdBozWEgBk7S62XKMs4u+sHEZ9YVJJG5psWkEbb5EdoSXTdo7Ntdo0hSuWYgMwdCJ3gjMTv3HKQclsqtBlUel4Lo9IafbhwxA4iPXcLW7LHb/Ga51xubnaUk2Mc4gNYXuPqtbck/cOtQ2Zsq12bSNO2bAObiTJMb3E8B1AZwBJRTpMotMdqSFteg6zos/WH3oR+2dmf6ql/wCRnzS85pe8up0867GnZz+/YVWaO9sz6wXRTRNdk5ocAcgSUw0DYG9wF14Kn0vsqFsLejSeYbhElo3QMwTu5Ll34cualfy9Wo2S7EYBO+TwjPrWS6LqF0XXOAIEL2MSpXRdRui6aIUrpXSuldNEJ3RdRui6cIhSSuldK6E4Urouo3TumhF0XRdK6E07pXQkhCldK6V0JoTuldRQhNSukkmhCwVVFHJ67A7g7Y8djhmtB+gISci8dRc0/YrVC2tptraFozBQrOa3hMgdQdIHYFYytUYIa4qsGgqcbn97mn4NC3aamZGLMYGX24cie0nM96zIULva19eNwXFZzm8CfR7WiB4JPqPf6xJQhCS18KCzuSTQoFRSQhCSE0kIQpJJIQhIIQhCEISKEIQhNCE0IKSEIQkmhCaaSEIQhCChCaEkFCEIQhCEIQhCEIQhCE0L/9k=', + }, + { + id: 5, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + ]; + + return ( + + + + { + history.replace({ + search: qs.stringify({ + ...query, + title: e.target.value ? e.target.value : undefined, + }), + }); + }} + /> +
+ {lendableObjects + .filter((lendableObject) => + query.title + ? lendableObject.title + .toLowerCase() + .includes(query.title.toLowerCase()) + : true + ) + .map((lendableObject) => ( + + ))} +
+
+ ); +}; + +export default LendableObjectsList; diff --git a/app/routes/lending/index.tsx b/app/routes/lending/index.tsx new file mode 100644 index 0000000000..f0dcc05af3 --- /dev/null +++ b/app/routes/lending/index.tsx @@ -0,0 +1,26 @@ +import { Route, Switch } from 'react-router-dom'; +import LendableObjectDetail from 'app/routes/lending/LendableObjectDetail'; +import LendableObjectsList from 'app/routes/lending/LendableObjectsList'; +import PageNotFound from 'app/routes/pageNotFound'; + +const lendingRoute = ({ + match, +}: { + match: { + path: string; + }; +}) => ( + + + + + +); + +export default function Lending() { + return ; +} diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts new file mode 100644 index 0000000000..11540ee34e --- /dev/null +++ b/app/store/models/LendableObject.d.ts @@ -0,0 +1,15 @@ +import type { ID } from 'app/store/models/index'; +import type { Duration } from 'moment-timezone'; + +interface LendableObject { + id: ID; + title: string; + description: string; + image: string; + hasContract: boolean; + maxLendingPeriod: Duration | string; +} + +export type ListLendableObject = Pick; +export type DetailedLendableObject = ListLendableObject & + Pick; diff --git a/app/styles/globals.css b/app/styles/globals.css index 0660917712..0e570f3511 100644 --- a/app/styles/globals.css +++ b/app/styles/globals.css @@ -91,8 +91,6 @@ thead { th, td { padding: 10px; - width: 140px; - display: table-cell; } th:first-child { diff --git a/package.json b/package.json index c094511599..7026e54b8b 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.15.3", "@reduxjs/toolkit": "^1.9.7", "@sentry/browser": "^7.80.0", diff --git a/yarn.lock b/yarn.lock index 79bfc50bba..e8e0415085 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1895,6 +1895,35 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== +"@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.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -11030,6 +11059,11 @@ postcss@^8.0.0, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.27, postcss@^8.4. picocolors "^1.0.0" source-map-js "^1.0.2" +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" From 7b589cd5cd98135e68ebf7648d36427a1dc5cd6e Mon Sep 17 00:00:00 2001 From: Juni Weisteen Bjerde Date: Thu, 12 Oct 2023 22:24:51 +0200 Subject: [PATCH 02/44] Add form modal on lending object page and start on admin page --- app/routes/lending/LendableObjectDetail.tsx | 73 +++++++++++++++++++-- app/routes/lending/LendableObjectsAdmin.css | 6 ++ app/routes/lending/LendableObjectsAdmin.tsx | 41 ++++++++++++ app/routes/lending/LendableObjectsList.tsx | 4 +- app/routes/lending/index.tsx | 6 ++ app/store/models/LendableObject.d.ts | 1 + 6 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 app/routes/lending/LendableObjectsAdmin.css create mode 100644 app/routes/lending/LendableObjectsAdmin.tsx diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/LendableObjectDetail.tsx index 518bcfe7e5..6ce0cd6b4d 100644 --- a/app/routes/lending/LendableObjectDetail.tsx +++ b/app/routes/lending/LendableObjectDetail.tsx @@ -2,13 +2,18 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import React from 'react'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; -import { Line } from 'recharts'; import { Content } from 'app/components/Content'; -import NavigationTab from 'app/components/NavigationTab'; +import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { DetailedLendableObject } from 'app/store/models/LendableObject'; +import { useState } from 'react'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import { createValidator, required } from 'app/utils/validation'; +import { Field } from 'react-final-form'; +import { Button, DatePicker, TextArea, TextInput } from 'app/components/Form'; +import Modal from 'app/components/Modal'; +import moment from 'moment-timezone'; type Params = { lendableObjectId: string; @@ -16,10 +21,17 @@ type Params = { const LendableObjectDetail = () => { const { lendableObjectId } = useParams(); + const [showLendingForm, setShowLendingForm] = useState(false); + const [startString, setStartString] = useState(''); + const [endString, setEndString] = useState(''); + + const onSubmit = () => {}; + const lendableObject: DetailedLendableObject = { id: lendableObjectId, title: 'Soundbox', description: 'En soundbox som kan brukes til å spille av lyder', + lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', image: 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', }; @@ -27,8 +39,12 @@ const LendableObjectDetail = () => { return ( - -

{lendableObject.description}

+ + + Godkjenn utlånsforespørsler + + +

{lendableObject.description}

{ right: 'timeGridWeek,dayGridMonth', }} select={(info) => { - console.log(info); + setStartString(info.startStr); + setEndString(info.endStr); + setShowLendingForm(true); }} /> + setShowLendingForm(false)}> + + {({ handleSubmit }) => { + return ( +
+ + + + + + ); + }} +
+
); }; +const validate = createValidator({ + responsiblePersonName: [required('Du må oppgi en avsvarsperson')], +}); + export default LendableObjectDetail; diff --git a/app/routes/lending/LendableObjectsAdmin.css b/app/routes/lending/LendableObjectsAdmin.css new file mode 100644 index 0000000000..87913ed809 --- /dev/null +++ b/app/routes/lending/LendableObjectsAdmin.css @@ -0,0 +1,6 @@ +.heading { + font-size: var(--font-size-lg); + border-bottom: 1px solid var(--border-gray); + padding-bottom: 10px; + margin-bottom: 20px; +} diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx new file mode 100644 index 0000000000..0b0a62b420 --- /dev/null +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -0,0 +1,41 @@ +import Card from 'app/components/Card'; +import { Content } from 'app/components/Content'; +import NavigationTab from 'app/components/NavigationTab'; +import { Helmet } from 'react-helmet-async'; +import styles from './LendableObjectsAdmin.css'; + +const PendingLendingRequest = () => { + return test; +}; + +const ApprovedLendingRequest = () => { + return test; +}; + +const LendableObjectsAdmin = () => { + const lendingRequests = [ + { user: '', startTime: '', endTime: '', approved: false, bitch: true }, + { user: '', startTime: '', endTime: '', approved: false }, + ]; + + return ( + + + +

Ventende utlånsforespørsler

+ {lendingRequests + .filter((request) => !request.approved) + .map((request) => ( + + ))} +

Godkjente utlånsforespørsler

+ {lendingRequests + .filter((request) => request.approved) + .map((request) => ( + + ))} +
+ ); +}; + +export default LendableObjectsAdmin; diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index 58fb301f4c..4cb1773a4e 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -7,7 +7,7 @@ import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; -import NavigationTab from 'app/components/NavigationTab'; +import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { ListLendableObject } from 'app/store/models/LendableObject'; import styles from './LendableObjectsList.css'; @@ -92,7 +92,7 @@ const LendableObjectsList = () => { return ( - + ( + Date: Thu, 19 Oct 2023 21:29:27 +0200 Subject: [PATCH 03/44] Add redux actions and form for creating lendable objects --- app/actions/ActionTypes.ts | 10 ++ app/actions/LendableObjectActions.ts | 74 +++++++++++ app/reducers/index.ts | 3 + app/reducers/lendableObjects.ts | 26 ++++ .../lending/LendableObjectAdminDetail.tsx | 125 ++++++++++++++++++ app/routes/lending/LendableObjectDetail.css | 0 app/routes/lending/LendableObjectDetail.tsx | 16 +-- app/routes/lending/LendableObjectEdit.tsx | 94 +++++++++++++ app/routes/lending/LendableObjectsAdmin.tsx | 46 +++++-- app/routes/lending/LendableObjectsList.css | 47 +++---- app/routes/lending/LendableObjectsList.tsx | 3 +- app/routes/lending/index.tsx | 13 ++ app/store/createRootReducer.ts | 2 + app/store/models/LendableObject.d.ts | 26 +++- app/store/models/entities.ts | 3 + 15 files changed, 439 insertions(+), 49 deletions(-) create mode 100644 app/actions/LendableObjectActions.ts create mode 100644 app/reducers/lendableObjects.ts create mode 100644 app/routes/lending/LendableObjectAdminDetail.tsx delete mode 100644 app/routes/lending/LendableObjectDetail.css create mode 100644 app/routes/lending/LendableObjectEdit.tsx diff --git a/app/actions/ActionTypes.ts b/app/actions/ActionTypes.ts index 8d20df5a36..162d1921c0 100644 --- a/app/actions/ActionTypes.ts +++ b/app/actions/ActionTypes.ts @@ -408,3 +408,13 @@ export const Reaction = { ADD: generateStatuses('Reaction.ADD') as AAT, DELETE: generateStatuses('Reaction.DELETE') as AAT, }; + +/** + * + */ +export const LendableObject = { + FETCH: generateStatuses('LendableObject.FETCH') as AAT, + CREATE: generateStatuses('LendableObject.CREATE') as AAT, + EDIT: generateStatuses('LendableObject.EDIT') as AAT, + DELETE: generateStatuses('LendableObject.DELETE') as AAT, +}; diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts new file mode 100644 index 0000000000..36bf9a6a58 --- /dev/null +++ b/app/actions/LendableObjectActions.ts @@ -0,0 +1,74 @@ +import callAPI from 'app/actions/callAPI'; +import { lendableObjectSchema } from 'app/reducers'; +import type { + EntityType, + NormalizedEntityPayload, +} from 'app/store/models/entities'; +import type { Thunk } from 'app/types'; +import { LendableObject } from './ActionTypes'; + +export function fetchAllLendableObjects(): Thunk< + Promise> +> { + return callAPI({ + types: LendableObject.FETCH, + endpoint: '/lendableobject/', + schema: [lendableObjectSchema], + meta: { + errorMessage: 'Henting av utlånsobjekter failet', + }, + propagateError: true, + }); +} + +export function fetchLendableObject(id: number): Thunk { + return callAPI({ + types: LendableObject.FETCH, + endpoint: `/lendableobject/${id}/`, + schema: lendableObjectSchema, + meta: { + errorMessage: 'Henting av utlånsobjekt feilet', + }, + propagateError: true, + }); +} + +export function deleteLendableObject(id: number): Thunk { + return callAPI({ + types: LendableObject.DELETE, + endpoint: `/lendableobject/${id}/`, + method: 'DELETE', + meta: { + id, + errorMessage: 'Sletting av utlånsobjekt feilet', + }, + }); +} + +export function createLendableObject(data: any): Thunk { + 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 +}: Record): Thunk { + return callAPI({ + types: LendableObject.EDIT, + endpoint: `/lendableobject/${id}/`, + method: 'PUT', + body: data, + meta: { + errorMessage: 'Endring av utlånsobjekt feilet', + }, + }); +} diff --git a/app/reducers/index.ts b/app/reducers/index.ts index 1dc88bdfd5..b4679a0990 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -132,3 +132,6 @@ export const followersCompanySchema = new schema.Entity( export const followersUserSchema = new schema.Entity(followersKeyGen('user'), { follower: userSchema, }); +export const lendableObjectSchema = new schema.Entity('lendableObjects', { + responsibleGroups: [groupSchema], +}); diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts new file mode 100644 index 0000000000..33136dc8b4 --- /dev/null +++ b/app/reducers/lendableObjects.ts @@ -0,0 +1,26 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { LendableObject } from 'app/actions/ActionTypes'; +import type { RootState } from 'app/store/createRootReducer'; +import createEntityReducer from 'app/utils/createEntityReducer'; +import type { EntityId } from '@reduxjs/toolkit'; + +export default createEntityReducer({ + key: 'lendableObjects', + types: { + fetch: LendableObject.FETCH, + mutate: LendableObject.CREATE, + delete: LendableObject.DELETE, + }, +}); +export const selectLendableObjects = createSelector( + (state: RootState) => state.lendableObjects.byId, + (state: RootState) => state.lendableObjects.items, + (lendableObjectsById, lendableObjectIds) => + lendableObjectIds.map((id) => lendableObjectsById[id]) +); +export const selectLendableObjectById = createSelector( + (state: RootState) => state.lendableObjects.byId, + (_: RootState, id: EntityId) => id, + (lendableObjectsById, lendableObjectId) => + lendableObjectsById[lendableObjectId] +); diff --git a/app/routes/lending/LendableObjectAdminDetail.tsx b/app/routes/lending/LendableObjectAdminDetail.tsx new file mode 100644 index 0000000000..a8ee78c2e4 --- /dev/null +++ b/app/routes/lending/LendableObjectAdminDetail.tsx @@ -0,0 +1,125 @@ +import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import moment from 'moment-timezone'; +import { Helmet } from 'react-helmet-async'; +import { useParams, Link } from 'react-router-dom'; +import { Content } from 'app/components/Content'; +import NavigationTab from 'app/components/NavigationTab'; +import type { DetailedLendableObject } from 'app/store/models/LendableObject'; + +type Params = { + lendableObjectId: string; +}; + +const LendableObjectAdminDetail = () => { + const { lendableObjectId } = useParams(); + + const lendingRequest = { + id: 1, + user: { + id: 1, + username: 'Eik', + fullName: 'Test Testesen', + }, + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + approved: false, + }; + + const lendableObject: DetailedLendableObject = { + id: lendableObjectId, + title: 'Soundbox', + description: 'En soundbox som kan brukes til å spille av lyder', + lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', + image: + 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', + }; + + const otherLoans = [ + { + id: 2, + startTime: moment().subtract({ days: 1, hours: 2 }), + endTime: moment().subtract({ hours: 8 }), + }, + { + id: 3, + startTime: moment().subtract({ hours: 6 }), + endTime: moment().subtract({ hours: 2 }), + }, + ]; + + const requestEvent = { + id: String(lendingRequest.id), + title: lendingRequest.user.fullName, + start: lendingRequest.startTime.toISOString(), + end: lendingRequest.endTime.toISOString(), + backgroundColor: '#e11617', + borderColor: '#e11617', + }; + + const otherLoanEvents = otherLoans.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#999999', + borderColor: '#999999', + })); + + const otherLoanRequests = [ + { + id: 5, + startTime: moment().subtract({ hours: 2 }), + endTime: moment().add({ hours: 2 }), + }, + ]; + + const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#f57676', + borderColor: '#f57676', + })); + + return ( + + + +

+ {lendingRequest.message} -{' '} + + {lendingRequest.user.fullName} + {' '} +

+ +
+ ); +}; + +export default LendableObjectAdminDetail; diff --git a/app/routes/lending/LendableObjectDetail.css b/app/routes/lending/LendableObjectDetail.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/LendableObjectDetail.tsx index 6ce0cd6b4d..4a280b99dd 100644 --- a/app/routes/lending/LendableObjectDetail.tsx +++ b/app/routes/lending/LendableObjectDetail.tsx @@ -2,18 +2,18 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; +import moment from 'moment-timezone'; +import { useState } from 'react'; +import { Field } from 'react-final-form'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; import { Content } from 'app/components/Content'; +import { Button, TextArea, TextInput } from 'app/components/Form'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import Modal from 'app/components/Modal'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { DetailedLendableObject } from 'app/store/models/LendableObject'; -import { useState } from 'react'; -import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import { createValidator, required } from 'app/utils/validation'; -import { Field } from 'react-final-form'; -import { Button, DatePicker, TextArea, TextInput } from 'app/components/Form'; -import Modal from 'app/components/Modal'; -import moment from 'moment-timezone'; type Params = { lendableObjectId: string; @@ -91,9 +91,7 @@ const LendableObjectDetail = () => { diff --git a/app/routes/lending/LendableObjectEdit.tsx b/app/routes/lending/LendableObjectEdit.tsx new file mode 100644 index 0000000000..46f3fed642 --- /dev/null +++ b/app/routes/lending/LendableObjectEdit.tsx @@ -0,0 +1,94 @@ +import { Field, FormSpy } from 'react-final-form'; +import { useParams } from 'react-router-dom'; +import { createLendableObject } from 'app/actions/LendableObjectActions'; +import { Content } from 'app/components/Content'; +import { + Button, + EditorField, + Form, + SelectInput, + TextInput, +} from 'app/components/Form'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import SubmissionError from 'app/components/Form/SubmissionError'; +import { useAppDispatch } from 'app/store/hooks'; +import { roleOptions } from 'app/utils/constants'; +import { spySubmittable } from 'app/utils/formSpyUtils'; + +type Params = { + lendableObjectId: string | undefined; +}; + +const LendableObjectEdit = () => { + const { lendableObjectId } = useParams(); + const isNew = lendableObjectId === undefined; + + const dispatch = useAppDispatch(); + + const onSubmit = (values) => + dispatch( + createLendableObject({ + ...values, + responsibleGroups: values.responsibleGroups.map((group) => group.id), + responsibleRoles: values.responsibleRoles.map((role) => role.value), + }) + ); + + return ( + + + {({ handleSubmit }) => ( +
+ + {(form) => { + return
{JSON.stringify(form.values, undefined, 2)}
; + }} +
+ + + + + + + {spySubmittable((submittable) => ( + + ))} + + )} +
+
+ ); +}; + +export default LendableObjectEdit; diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx index 0b0a62b420..d855493612 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -1,21 +1,49 @@ +import moment from 'moment-timezone'; +import { Helmet } from 'react-helmet-async'; import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; -import { Helmet } from 'react-helmet-async'; import styles from './LendableObjectsAdmin.css'; -const PendingLendingRequest = () => { - return test; +type LendingRequestProps = { + pending: boolean; + request: any; }; -const ApprovedLendingRequest = () => { - return test; +const LendingRequest = ({ request }: LendingRequestProps) => { + return ( + + {request.lendableObject.title} - {request.user} + + ); }; const LendableObjectsAdmin = () => { const lendingRequests = [ - { user: '', startTime: '', endTime: '', approved: false, bitch: true }, - { user: '', startTime: '', endTime: '', approved: false }, + { + id: 1, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + approved: false, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 2, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + approved: false, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, ]; return ( @@ -26,13 +54,13 @@ const LendableObjectsAdmin = () => { {lendingRequests .filter((request) => !request.approved) .map((request) => ( - + ))}

Godkjente utlånsforespørsler

{lendingRequests .filter((request) => request.approved) .map((request) => ( - + ))}
); diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/LendableObjectsList.css index f6ca193daf..f89666897b 100644 --- a/app/routes/lending/LendableObjectsList.css +++ b/app/routes/lending/LendableObjectsList.css @@ -1,43 +1,44 @@ @import url('~app/styles/variables.css'); .searchBar { - margin-bottom: 2rem; + margin-bottom: 2rem; } .lendableObjectsContainer { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-gap: 2rem; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 2rem; } .lendableObjectCard { - display: flex; - flex-direction: column; - align-items: stretch; - padding: 0; + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0; } .lendableObjectImage { - height: 15rem; - object-fit: cover; + height: 15rem; + object-fit: cover; } .lendableObjectFooter { - display: flex; - justify-content: center; - color: var(--lego-font-color); - font-size: 1.1rem; - font-weight: bold; - background-color: var(--color-gray-1); + display: flex; + justify-content: center; + color: var(--lego-font-color); + font-size: 1.1rem; + font-weight: bold; + background-color: var(--color-gray-1); } @media (--medium-viewport) { - .lendableObjectsContainer { - grid-template-columns: repeat(2, 1fr); - } + .lendableObjectsContainer { + grid-template-columns: repeat(2, 1fr); + } } + @media (--small-viewport) { - .lendableObjectsContainer { - grid-template-columns: 1fr; - } -} \ No newline at end of file + .lendableObjectsContainer { + grid-template-columns: 1fr; + } +} diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index 4cb1773a4e..a1f0549058 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -1,13 +1,12 @@ import { usePreparedEffect } from '@webkom/react-prepare'; import qs from 'qs'; -import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useHistory, useLocation } from 'react-router-dom'; import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; -import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import NavigationTab from 'app/components/NavigationTab'; import type { ListLendableObject } from 'app/store/models/LendableObject'; import styles from './LendableObjectsList.css'; diff --git a/app/routes/lending/index.tsx b/app/routes/lending/index.tsx index f0789fe0c9..72016352c3 100644 --- a/app/routes/lending/index.tsx +++ b/app/routes/lending/index.tsx @@ -1,7 +1,9 @@ import { Route, Switch } from 'react-router-dom'; import LendableObjectDetail from 'app/routes/lending/LendableObjectDetail'; +import LendableObjectEdit from 'app/routes/lending/LendableObjectEdit'; import LendableObjectsList from 'app/routes/lending/LendableObjectsList'; import PageNotFound from 'app/routes/pageNotFound'; +import LendableObjectAdminDetail from './LendableObjectAdminDetail'; import LendableObjectsAdmin from './LendableObjectsAdmin'; const lendingRoute = ({ @@ -13,11 +15,22 @@ const lendingRoute = ({ }) => ( + + + { galleryPictures, groups, joblistings, + lendableObjects, meetingInvitations, meetings, meetingsToken, diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts index 3296874820..6f149c2e60 100644 --- a/app/store/models/LendableObject.d.ts +++ b/app/store/models/LendableObject.d.ts @@ -1,16 +1,30 @@ -import type { ID } from 'app/store/models/index'; +import type { RoleType } from 'app/utils/constants'; +import type { EntityId } from '@reduxjs/toolkit'; import type { Duration } from 'moment-timezone'; interface LendableObject { - id: ID; + id: EntityId; + image: string; title: string; description: string; - image: string; - lendingCommentPromt: string; + location: string; hasContract: boolean; - maxLendingPeriod: Duration | string; + maxLendingPeriod: null | string | Duration; + //lendingCommentPrompt: string; + responsibleRoles: RoleType[]; + responsibleGroups: EntityId[]; } export type ListLendableObject = Pick; export type DetailedLendableObject = ListLendableObject & - Pick; + Pick< + LendableObject, + | 'description' + | 'location' + | 'hasContract' + | 'maxLendingPeriod' + | 'responsibleRoles' + | 'responsibleGroups' + >; + +export type UnknownLendableObject = ListLendableObject | DetailedLendableObject; diff --git a/app/store/models/entities.ts b/app/store/models/entities.ts index 67d6172950..133168c4e4 100644 --- a/app/store/models/entities.ts +++ b/app/store/models/entities.ts @@ -15,6 +15,7 @@ import type { UnknownGallery } from 'app/store/models/Gallery'; import type { UnknownGalleryPicture } from 'app/store/models/GalleryPicture'; import type { UnknownGroup } from 'app/store/models/Group'; 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'; @@ -50,6 +51,7 @@ export enum EntityType { GalleryPictures = 'galleryPictures', Groups = 'groups', Joblistings = 'joblistings', + LendableObjects = 'lendableObjects', MeetingInvitations = 'meetingInvitations', Meetings = 'meetings', Memberships = 'memberships', @@ -90,6 +92,7 @@ export default interface Entities { [EntityType.GalleryPictures]: Record; [EntityType.Groups]: Record; [EntityType.Joblistings]: Record; + [EntityType.LendableObjects]: Record; [EntityType.MeetingInvitations]: Record; [EntityType.Meetings]: Record; [EntityType.Memberships]: Record; From 04cc60867baaf6c285af8f70a184dac2f37a8a9d Mon Sep 17 00:00:00 2001 From: Juni Weisteen Bjerde Date: Tue, 7 Nov 2023 22:09:51 +0100 Subject: [PATCH 04/44] Change Card and Modal imports to use lego-bricks --- app/routes/lending/LendableObjectDetail.tsx | 2 +- app/routes/lending/LendableObjectsAdmin.tsx | 2 +- app/routes/lending/LendableObjectsList.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/LendableObjectDetail.tsx index 4a280b99dd..46fb6b8007 100644 --- a/app/routes/lending/LendableObjectDetail.tsx +++ b/app/routes/lending/LendableObjectDetail.tsx @@ -2,6 +2,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; +import { Modal } from '@webkom/lego-bricks'; import moment from 'moment-timezone'; import { useState } from 'react'; import { Field } from 'react-final-form'; @@ -10,7 +11,6 @@ import { useParams } from 'react-router-dom'; import { Content } from 'app/components/Content'; import { Button, TextArea, TextInput } from 'app/components/Form'; import LegoFinalForm from 'app/components/Form/LegoFinalForm'; -import Modal from 'app/components/Modal'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { DetailedLendableObject } from 'app/store/models/LendableObject'; import { createValidator, required } from 'app/utils/validation'; diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx index d855493612..a71c64ea22 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -1,6 +1,6 @@ +import { Card } from '@webkom/lego-bricks'; import moment from 'moment-timezone'; import { Helmet } from 'react-helmet-async'; -import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; import styles from './LendableObjectsAdmin.css'; diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index a1f0549058..e64f938cbe 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -1,8 +1,8 @@ +import { Card } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import qs from 'qs'; import { Helmet } from 'react-helmet-async'; import { Link, useHistory, useLocation } from 'react-router-dom'; -import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; From c442c610fbc27bac3720860613258ca3484f9fe1 Mon Sep 17 00:00:00 2001 From: Juni Weisteen Bjerde Date: Tue, 7 Nov 2023 23:27:38 +0100 Subject: [PATCH 05/44] Fix most comments --- .../lending/LendableObjectAdminDetail.tsx | 6 +++--- app/routes/lending/LendableObjectDetail.tsx | 20 ++++++++++--------- app/routes/lending/LendableObjectEdit.tsx | 2 +- app/routes/lending/LendableObjectsList.css | 4 ++-- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/routes/lending/LendableObjectAdminDetail.tsx b/app/routes/lending/LendableObjectAdminDetail.tsx index a8ee78c2e4..c7f5686ad4 100644 --- a/app/routes/lending/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/LendableObjectAdminDetail.tsx @@ -89,15 +89,15 @@ const LendableObjectAdminDetail = () => { return ( - + +

{lendingRequest.message} -{' '} {lendingRequest.user.fullName} {' '}

+ { const { lendableObjectId } = useParams(); const [showLendingForm, setShowLendingForm] = useState(false); - const [startString, setStartString] = useState(''); - const [endString, setEndString] = useState(''); + const [start, setstart] = useState(''); + const [end, setend] = useState(''); const onSubmit = () => {}; @@ -39,12 +39,15 @@ const LendableObjectDetail = () => { return ( + Godkjenn utlånsforespørsler -

{lendableObject.description}

+ +

{lendableObject.description}

+ { right: 'timeGridWeek,dayGridMonth', }} select={(info) => { - setStartString(info.startStr); - setEndString(info.endStr); + setstart(info.startStr); + setend(info.endStr); setShowLendingForm(true); }} /> + setShowLendingForm(false)}> { diff --git a/app/routes/lending/LendableObjectEdit.tsx b/app/routes/lending/LendableObjectEdit.tsx index 46f3fed642..a72fe5ebbb 100644 --- a/app/routes/lending/LendableObjectEdit.tsx +++ b/app/routes/lending/LendableObjectEdit.tsx @@ -74,7 +74,7 @@ const LendableObjectEdit = () => { /> diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/LendableObjectsList.css index f89666897b..cf3636395c 100644 --- a/app/routes/lending/LendableObjectsList.css +++ b/app/routes/lending/LendableObjectsList.css @@ -27,8 +27,8 @@ justify-content: center; color: var(--lego-font-color); font-size: 1.1rem; - font-weight: bold; - background-color: var(--color-gray-1); + font-weight: 500; + background-color: var(--additive-background); } @media (--medium-viewport) { From c87c0c2e3f5abf08620155d70bcf7df99c521ee0 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 16 Jan 2024 19:31:40 +0100 Subject: [PATCH 06/44] Replace custom header style with header tag --- app/routes/lending/LendableObjectsList.css | 2 -- app/routes/lending/LendableObjectsList.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/LendableObjectsList.css index cf3636395c..c8ec8cb80b 100644 --- a/app/routes/lending/LendableObjectsList.css +++ b/app/routes/lending/LendableObjectsList.css @@ -26,8 +26,6 @@ display: flex; justify-content: center; color: var(--lego-font-color); - font-size: 1.1rem; - font-weight: 500; background-color: var(--additive-background); } diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index e64f938cbe..90001ca7df 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -24,7 +24,7 @@ const LendableObject = ({ alt={`${lendableObject.title}`} />
-

{lendableObject.title}

+

{lendableObject.title}

From 8e580ba144ee38cc6a43929b075d172e0f6cc383 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 16 Jan 2024 19:43:06 +0100 Subject: [PATCH 07/44] Remove unnecessary commenting --- app/actions/ActionTypes.ts | 90 -------------------------------------- 1 file changed, 90 deletions(-) diff --git a/app/actions/ActionTypes.ts b/app/actions/ActionTypes.ts index 162d1921c0..0cf6662ebf 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 = { CLEAR: 'Event.CLEAR', FETCH: generateStatuses('Event.FETCH') as AAT, @@ -39,9 +36,6 @@ export const Event = { IS_USER_FOLLOWING: generateStatuses('Event.IS_USER_FOLLOWING') as AAT, }; -/** - * - */ export const Article = { FETCH: generateStatuses('Article.FETCH') as AAT, CREATE: generateStatuses('Article.CREATE') as AAT, @@ -49,36 +43,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, @@ -91,9 +73,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, @@ -104,9 +83,6 @@ export const GalleryPicture = { CLEAR: 'GalleryPicture.CLEAR', }; -/** - * - */ export const Joblistings = { FETCH: generateStatuses('Joblistings.FETCH') as AAT, CREATE: generateStatuses('Joblistings.CREATE') as AAT, @@ -114,9 +90,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, @@ -124,9 +97,6 @@ export const Announcements = { DELETE: generateStatuses('Announcements.DELETE') as AAT, }; -/** - * - */ export const Meeting = { FETCH: generateStatuses('Meeting.FETCH') as AAT, SET_INVITATION_STATUS: generateStatuses( @@ -141,9 +111,6 @@ export const Meeting = { RESET_MEETINGS_TOKEN: 'Meeting.RESET_MEETINGS_TOKEN', }; -/** - * - */ export const Group = { FETCH: generateStatuses('Group.FETCH') as AAT, UPDATE: generateStatuses('Group.UPDATE') as AAT, @@ -167,24 +134,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( @@ -208,9 +166,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, @@ -222,9 +177,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, @@ -242,9 +194,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, @@ -274,9 +223,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, @@ -284,16 +230,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, @@ -302,9 +242,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, @@ -318,32 +255,20 @@ 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 Routing = { SET_STATUS_CODE: 'Routing.SET_STATUS_CODE', }; -/** - * - */ export const OAuth2 = { FETCH_APPLICATIONS: generateStatuses('OAuth2.FETCH_APPLICATIONS') as AAT, FETCH_APPLICATION: generateStatuses('OAuth2.FETCH_APPLICATION') as AAT, @@ -353,9 +278,6 @@ export const OAuth2 = { DELETE_GRANT: generateStatuses('OAuth2.DELETE_GRANT') as AAT, }; -/** - * - */ export const NotificationSettings = { FETCH_ALTERNATIVES: generateStatuses( 'NotificationSettings.FETCH_ALTERNATIVES' @@ -364,16 +286,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, }; @@ -401,17 +317,11 @@ 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: generateStatuses('LendableObject.FETCH') as AAT, CREATE: generateStatuses('LendableObject.CREATE') as AAT, From 28637fe19ce24f7278fb6f66c6ee8d497bb83bad Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 16 Jan 2024 20:39:10 +0100 Subject: [PATCH 08/44] Add navigation to request approval --- app/routes/lending/LendableObjectsAdmin.css | 9 +++++++++ app/routes/lending/LendableObjectsAdmin.tsx | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/routes/lending/LendableObjectsAdmin.css b/app/routes/lending/LendableObjectsAdmin.css index 87913ed809..954f70a0e8 100644 --- a/app/routes/lending/LendableObjectsAdmin.css +++ b/app/routes/lending/LendableObjectsAdmin.css @@ -4,3 +4,12 @@ padding-bottom: 10px; margin-bottom: 20px; } + +.lendingRequest { + margin-bottom: 8px; + cursor: pointer; +} + +.navLink { + text-decoration: none; +} diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx index a71c64ea22..91a929a29e 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -2,8 +2,9 @@ import { Card } from '@webkom/lego-bricks'; import moment from 'moment-timezone'; import { Helmet } from 'react-helmet-async'; import { Content } from 'app/components/Content'; -import NavigationTab from 'app/components/NavigationTab'; +import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import styles from './LendableObjectsAdmin.css'; +import { NavLink } from 'react-router-dom'; type LendingRequestProps = { pending: boolean; @@ -12,9 +13,11 @@ type LendingRequestProps = { const LendingRequest = ({ request }: LendingRequestProps) => { return ( - - {request.lendableObject.title} - {request.user} - + + + {request.lendableObject.title} - {request.user} + + ); }; From 87c872fb23b63f3821e36c5ff3e62b2aa64f8c8e Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 16 Jan 2024 20:43:24 +0100 Subject: [PATCH 09/44] Remove form spy debug --- app/routes/lending/LendableObjectEdit.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/routes/lending/LendableObjectEdit.tsx b/app/routes/lending/LendableObjectEdit.tsx index a72fe5ebbb..cd71aff084 100644 --- a/app/routes/lending/LendableObjectEdit.tsx +++ b/app/routes/lending/LendableObjectEdit.tsx @@ -39,11 +39,6 @@ const LendableObjectEdit = () => { {({ handleSubmit }) => (
- - {(form) => { - return
{JSON.stringify(form.values, undefined, 2)}
; - }} -
Date: Tue, 16 Jan 2024 20:45:51 +0100 Subject: [PATCH 10/44] Replace browse button with edit button --- app/routes/lending/LendableObjectDetail.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/LendableObjectDetail.tsx index ca0b2a7748..89f562e075 100644 --- a/app/routes/lending/LendableObjectDetail.tsx +++ b/app/routes/lending/LendableObjectDetail.tsx @@ -41,8 +41,8 @@ const LendableObjectDetail = () => { - - Godkjenn utlånsforespørsler + + Rediger From de8e4bd475bb2c419bcfd875a299c2566632af65 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 16 Jan 2024 20:49:13 +0100 Subject: [PATCH 11/44] Add buttons to create and approve objects --- app/routes/lending/LendableObjectsList.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index 90001ca7df..7699557c9d 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -6,7 +6,7 @@ import { Link, useHistory, useLocation } from 'react-router-dom'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; -import NavigationTab from 'app/components/NavigationTab'; +import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { ListLendableObject } from 'app/store/models/LendableObject'; import styles from './LendableObjectsList.css'; @@ -91,7 +91,11 @@ const LendableObjectsList = () => { return ( - + + Nytt utlånsobjekt + Godkjenn utlånsobjekter + + Date: Thu, 1 Feb 2024 23:06:30 +0100 Subject: [PATCH 12/44] Graphical interface for admin and "my requests" --- app/routes/lending/LendableObjectsAdmin.css | 34 +++- app/routes/lending/LendableObjectsAdmin.tsx | 178 +++++++++++++++++--- app/routes/lending/LendableObjectsList.css | 29 ++++ app/routes/lending/LendableObjectsList.tsx | 154 ++++++++++++++++- app/routes/lending/index.tsx | 4 +- 5 files changed, 374 insertions(+), 25 deletions(-) diff --git a/app/routes/lending/LendableObjectsAdmin.css b/app/routes/lending/LendableObjectsAdmin.css index 954f70a0e8..709dbfa9e7 100644 --- a/app/routes/lending/LendableObjectsAdmin.css +++ b/app/routes/lending/LendableObjectsAdmin.css @@ -6,10 +6,42 @@ } .lendingRequest { - margin-bottom: 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 15px; + border-radius: var(--border-radius-lg); + color: var(--lego-font-color); + transition: background-color var(--easing-fast); +} + +.notPending { cursor: pointer; } .navLink { text-decoration: none; } + +.newLendableObject { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px solid var(--color-green-6); + color: var(--color-green-6); + transition: var(--easing-fast); + padding: 15px; + + &:hover { + background-color: var(--color-green-6); + color: var(--color-absolute-white); + } +} + +.lendableObject { + display: flex; + align-items: center; + justify-content: space-between; +} \ No newline at end of file diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx index 91a929a29e..bcdc9e6512 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -1,23 +1,60 @@ -import { Card } from '@webkom/lego-bricks'; +import { Button, Card, Flex, Icon } from '@webkom/lego-bricks'; +import cx from 'classnames'; import moment from 'moment-timezone'; import { Helmet } from 'react-helmet-async'; +import { Link } from 'react-router-dom'; import { Content } from 'app/components/Content'; -import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import NavigationTab from 'app/components/NavigationTab'; import styles from './LendableObjectsAdmin.css'; -import { NavLink } from 'react-router-dom'; +import { useState } from 'react'; +import { ListLendableObject } from 'app/store/models/LendableObject'; type LendingRequestProps = { pending: boolean; request: any; }; +enum status { + PENDING, + APPROVED, + DENIED, +} + const LendingRequest = ({ request }: LendingRequestProps) => { + const isPending = request.status === status.PENDING; + const [isOpen, setIsOpen] = useState(isPending); + return ( - - - {request.lendableObject.title} - {request.user} - - + { + if (!isPending) { + setIsOpen(!isOpen); + } + }} + > + +

+ {request.lendableObject.title} - {request.user} - {request.id} +

+ {isOpen && ( + +

{request.message}

+

+ {request.startTime.format('DD.MM.YYYY HH:mm')} - {request.endTime.format('DD.MM.YYYY HH:mm')} +

+
+ )} +
+ {isPending && ( + + + + + )} +
); }; @@ -28,7 +65,8 @@ const LendableObjectsAdmin = () => { user: 'Test Testesen', startTime: moment().subtract({ hours: 2 }), endTime: moment(), - approved: false, + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: status.PENDING, lendableObject: { id: 1, title: 'Grill', @@ -40,31 +78,133 @@ const LendableObjectsAdmin = () => { user: 'Test Testesen', startTime: moment().subtract({ hours: 2 }), endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, + status: status.PENDING, lendableObject: { id: 2, title: 'Grill', image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, }, + { + id: 3, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.APPROVED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 4, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.DENIED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + ]; + + const lendableObjects: Array = [ + { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 2, + title: 'Soundboks', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 3, + title: 'Soundboks2', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 4, + title: 'Prinsessekjole', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 5, + title: 'Falk', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, ]; + const [showFetchMore, setShowFetchMore] = useState(true); + return ( - +

Ventende utlånsforespørsler

- {lendingRequests - .filter((request) => !request.approved) - .map((request) => ( - - ))} + + {lendingRequests + .filter((request) => request.status === status.PENDING) + .map((request) => ( + + ))} + +

Godkjente utlånsforespørsler

- {lendingRequests - .filter((request) => request.approved) - .map((request) => ( - + + {lendingRequests + .filter((request) => request.status !== status.PENDING) + .map((request) => ( + + ))} + {showFetchMore && ( + + )} + +
+

Utlånsobjekter

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

{lendableObject.id} - {lendableObject.title}

+ + + +
))} +
); }; diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/LendableObjectsList.css index c8ec8cb80b..7c2d3d999e 100644 --- a/app/routes/lending/LendableObjectsList.css +++ b/app/routes/lending/LendableObjectsList.css @@ -1,5 +1,34 @@ @import url('~app/styles/variables.css'); +.request { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.status { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px 25px; + border-radius: var(--border-radius-lg); + font-weight: 400; +} + +.pending { + background-color: var(--color-orange-6); +} + +.approved { + background-color: var(--color-green-6); +} + +.denied { + background-color: var(--color-red-6); +} + .searchBar { margin-bottom: 2rem; } diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index 7699557c9d..3fc89b70ae 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -1,6 +1,7 @@ -import { Card } from '@webkom/lego-bricks'; +import { Button, Card, Flex, Icon } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import qs from 'qs'; +import cx from 'classnames'; import { Helmet } from 'react-helmet-async'; import { Link, useHistory, useLocation } from 'react-router-dom'; import { Content } from 'app/components/Content'; @@ -9,6 +10,8 @@ import { Image } from 'app/components/Image'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { ListLendableObject } from 'app/store/models/LendableObject'; import styles from './LendableObjectsList.css'; +import moment from 'moment'; +import { useState } from 'react'; const LendableObject = ({ lendableObject, @@ -45,6 +48,75 @@ const parseQuery = (search: string): Query => { }; }; +enum status { + PENDING = "pending", + APPROVED = "approved", + DENIED = "denied", +} + +const ApprovedFlag = () => { + return ( +
+ + Godkjent! +
+ ) +} + +const PendingFlag = () => { + return ( +
+ + Venter på svar +
+ ) +} + +const DeniedFlag = () => { + return ( +
+ + Avslått.. +
+ ) +} + +const Request = ({ request }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(!isOpen)} + > + +

{request.lendableObject.title}

+ {isOpen && ( + <> +

{request.message}

+

+ {request.startTime.format('DD.MM.YYYY HH:mm')} - {request.endTime.format('DD.MM.YYYY HH:mm')} +

+ {request.status === status.PENDING && ( + + )} + + )} +
+ + { + request.status === status.PENDING ? : + request.status === status.APPROVED ? : + + } + +
+ ) +} + const LendableObjectsList = () => { const location = useLocation(); const query = parseQuery(location.search); @@ -87,15 +159,91 @@ const LendableObjectsList = () => { image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, ]; + const myRequests = [ + { + id: 1, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: status.PENDING, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 2, + user: 'Test Testesen', + startTime: moment().subtract({ days: 2 }), + endTime: moment().subtract({ days: 1 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.DENIED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 3, + user: 'Test Testesen', + startTime: moment().add({ hours: 2 }), + endTime: moment().add({ hours: 4 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.APPROVED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 3, + user: 'Test Testesen', + startTime: moment().add({ hours: 2 }), + endTime: moment().add({ hours: 4 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.DENIED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + ] + + const [showFetchMore, setShowFetchMore] = useState(false); return ( - Nytt utlånsobjekt - Godkjenn utlånsobjekter + Admin +

Mine forespørsler

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

Her var det tomt!

+ ) : ( + myRequests + .filter((request) => request.endTime.isAfter(moment().startOf('day'))) + // sorting? + .map((request) => ) + )} + +
+ {showFetchMore ? ( + + ) : ( + + )} + +

Utlånsobjekter

Date: Sat, 3 Feb 2024 14:34:27 +0100 Subject: [PATCH 13/44] Move request to own component and minor gui fixes --- app/routes/lending/LendableObjectsAdmin.css | 4 +- app/routes/lending/LendableObjectsAdmin.tsx | 89 ++------ app/routes/lending/LendableObjectsList.css | 29 +-- app/routes/lending/LendableObjectsList.tsx | 211 ++++++------------ .../lending/components/LendingRequest.css | 39 ++++ .../lending/components/LendingRequest.tsx | 91 ++++++++ 6 files changed, 227 insertions(+), 236 deletions(-) create mode 100644 app/routes/lending/components/LendingRequest.css create mode 100644 app/routes/lending/components/LendingRequest.tsx diff --git a/app/routes/lending/LendableObjectsAdmin.css b/app/routes/lending/LendableObjectsAdmin.css index 709dbfa9e7..3b3a8597a0 100644 --- a/app/routes/lending/LendableObjectsAdmin.css +++ b/app/routes/lending/LendableObjectsAdmin.css @@ -1,3 +1,5 @@ +@import url('~app/styles/variables.css'); + .heading { font-size: var(--font-size-lg); border-bottom: 1px solid var(--border-gray); @@ -44,4 +46,4 @@ display: flex; align-items: center; justify-content: space-between; -} \ No newline at end of file +} diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx index bcdc9e6512..1182bb37e8 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -1,5 +1,4 @@ import { Button, Card, Flex, Icon } from '@webkom/lego-bricks'; -import cx from 'classnames'; import moment from 'moment-timezone'; import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; @@ -8,55 +7,7 @@ import NavigationTab from 'app/components/NavigationTab'; import styles from './LendableObjectsAdmin.css'; import { useState } from 'react'; import { ListLendableObject } from 'app/store/models/LendableObject'; - -type LendingRequestProps = { - pending: boolean; - request: any; -}; - -enum status { - PENDING, - APPROVED, - DENIED, -} - -const LendingRequest = ({ request }: LendingRequestProps) => { - const isPending = request.status === status.PENDING; - const [isOpen, setIsOpen] = useState(isPending); - - return ( - { - if (!isPending) { - setIsOpen(!isOpen); - } - }} - > - -

- {request.lendableObject.title} - {request.user} - {request.id} -

- {isOpen && ( - -

{request.message}

-

- {request.startTime.format('DD.MM.YYYY HH:mm')} - {request.endTime.format('DD.MM.YYYY HH:mm')} -

-
- )} -
- {isPending && ( - - - - - )} -
- ); -}; +import { LendingRequest, status } from './components/LendingRequest'; const LendableObjectsAdmin = () => { const lendingRequests = [ @@ -150,52 +101,48 @@ const LendableObjectsAdmin = () => { return ( -

Ventende utlånsforespørsler

- + {lendingRequests .filter((request) => request.status === status.PENDING) .map((request) => ( - + ))} -
-

Godkjente utlånsforespørsler

- + +

Tidligere utlånsforespørsler

+ {lendingRequests .filter((request) => request.status !== status.PENDING) .map((request) => ( - + ))} - {showFetchMore && ( - - )} + {showFetchMore && } -
+

Utlånsobjekter

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

{lendableObject.id} - {lendableObject.title}

+

+ {lendableObject.id} - {lendableObject.title} +

- )} - - )} -
- - { - request.status === status.PENDING ? : - request.status === status.APPROVED ? : - - } - - - ) -} - -const LendableObjectsList = () => { +export const LendableObjectsList = () => { const location = useLocation(); const query = parseQuery(location.search); const history = useHistory(); @@ -160,62 +91,62 @@ const LendableObjectsList = () => { }, ]; const myRequests = [ - { + { + id: 1, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: status.PENDING, + lendableObject: { id: 1, - user: 'Test Testesen', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - status: status.PENDING, - lendableObject: { - id: 1, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, - { + }, + { + id: 2, + user: 'Test Testesen', + startTime: moment().subtract({ days: 2 }), + endTime: moment().subtract({ days: 1 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.DENIED, + lendableObject: { id: 2, - user: 'Test Testesen', - startTime: moment().subtract({ days: 2 }), - endTime: moment().subtract({ days: 1 }), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: status.DENIED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, - { - id: 3, - user: 'Test Testesen', - startTime: moment().add({ hours: 2 }), - endTime: moment().add({ hours: 4 }), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: status.APPROVED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, + }, + { + id: 3, + user: 'Test Testesen', + startTime: moment().add({ hours: 2 }), + endTime: moment().add({ hours: 4 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.APPROVED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, - { - id: 3, - user: 'Test Testesen', - startTime: moment().add({ hours: 2 }), - endTime: moment().add({ hours: 4 }), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: status.DENIED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, + }, + { + id: 3, + user: 'Test Testesen', + startTime: moment().add({ hours: 2 }), + endTime: moment().add({ hours: 4 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + approved: false, + status: status.DENIED, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, - ] + }, + ]; const [showFetchMore, setShowFetchMore] = useState(false); @@ -227,23 +158,29 @@ const LendableObjectsList = () => {

Mine forespørsler

- {myRequests.length === 0 ? ( -

Her var det tomt!

- ) : ( - myRequests - .filter((request) => request.endTime.isAfter(moment().startOf('day'))) - // sorting? - .map((request) => ) - )} +
+ {myRequests.length === 0 ? ( +

Her var det tomt!

+ ) : ( + myRequests + .filter((request) => + request.endTime.isAfter(moment().startOf('day')) + ) + // sorting? + .map((request) => ( + + )) + )} +
-
- {showFetchMore ? ( - - ) : ( - - )} + {myRequests.length !== 0 && + (showFetchMore ? ( + + ) : ( + + ))} -

Utlånsobjekter

+

Utlånsobjekter

{ + return ( +
+ + Godkjent! +
+ ); +}; + +const PendingFlag = () => { + return ( +
+ + Venter på svar +
+ ); +}; + +const DeniedFlag = () => { + return ( +
+ + Avslått +
+ ); +}; + +type LendingRequestProps = { + request: any; + isAdmin?: boolean; +}; + +export const LendingRequest = ({ + request, + isAdmin = false, +}: LendingRequestProps) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(!isOpen)} + > + +

{request.lendableObject.title}

+ {isOpen && ( + <> +

{request.message}

+

+ {request.startTime.format('DD.MM.YYYY HH:mm')} -{' '} + {request.endTime.format('DD.MM.YYYY HH:mm')} +

+ {request.status === status.PENDING && ( + + )} + + )} +
+ + {request.status === status.APPROVED ? ( + + ) : request.status === status.DENIED ? ( + + ) : isAdmin ? ( + + + + + ) : ( + + )} + +
+ ); +}; + +export default LendingRequest; From 394baa6a42988ee33811cd3af6430d5dd04325e8 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Sun, 4 Feb 2024 14:40:23 +0100 Subject: [PATCH 14/44] Rebase and update to use react-router v6 --- app/routes/index.tsx | 2 + .../LendableObjectAdminDetail.tsx | 0 .../{ => components}/LendableObjectDetail.tsx | 2 +- .../{ => components}/LendableObjectEdit.tsx | 0 .../{ => components}/LendableObjectsList.css | 0 .../{ => components}/LendableObjectsList.tsx | 55 ++++-------------- .../LendingAdmin.css} | 0 .../LendingAdmin.tsx} | 8 +-- app/routes/lending/index.tsx | 58 ++++++------------- app/store/models/LendableObject.d.ts | 2 +- 10 files changed, 36 insertions(+), 91 deletions(-) rename app/routes/lending/{ => components}/LendableObjectAdminDetail.tsx (100%) rename app/routes/lending/{ => components}/LendableObjectDetail.tsx (100%) rename app/routes/lending/{ => components}/LendableObjectEdit.tsx (100%) rename app/routes/lending/{ => components}/LendableObjectsList.css (100%) rename app/routes/lending/{ => components}/LendableObjectsList.tsx (96%) rename app/routes/lending/{LendableObjectsAdmin.css => components/LendingAdmin.css} (100%) rename app/routes/lending/{LendableObjectsAdmin.tsx => components/LendingAdmin.tsx} (96%) diff --git a/app/routes/index.tsx b/app/routes/index.tsx index 61b596a267..b97655bbd2 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -16,6 +16,7 @@ const ContactRoute = loadable(() => import('./contact')); const EventsRoute = loadable(() => import('./events')); const InterestGroupsRoute = loadable(() => import('./interestgroups')); const JoblistingsRoute = loadable(() => import('./joblistings')); +const LendingRoute = loadable(() => import('./lending')); const MeetingsRoute = loadable(() => import('./meetings')); const Overview = loadable(() => import('./overview')); const PageNotFound = loadable(() => import('./pageNotFound')); @@ -49,6 +50,7 @@ const RouterConfig = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/app/routes/lending/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx similarity index 100% rename from app/routes/lending/LendableObjectAdminDetail.tsx rename to app/routes/lending/components/LendableObjectAdminDetail.tsx diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx similarity index 100% rename from app/routes/lending/LendableObjectDetail.tsx rename to app/routes/lending/components/LendableObjectDetail.tsx index 89f562e075..3f582320e4 100644 --- a/app/routes/lending/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -12,8 +12,8 @@ import { Content } from 'app/components/Content'; import { Button, TextArea, TextInput } from 'app/components/Form'; import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; -import type { DetailedLendableObject } from 'app/store/models/LendableObject'; import { createValidator, required } from 'app/utils/validation'; +import type { DetailedLendableObject } from 'app/store/models/LendableObject'; type Params = { lendableObjectId: string; diff --git a/app/routes/lending/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx similarity index 100% rename from app/routes/lending/LendableObjectEdit.tsx rename to app/routes/lending/components/LendableObjectEdit.tsx diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/components/LendableObjectsList.css similarity index 100% rename from app/routes/lending/LendableObjectsList.css rename to app/routes/lending/components/LendableObjectsList.css diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx similarity index 96% rename from app/routes/lending/LendableObjectsList.tsx rename to app/routes/lending/components/LendableObjectsList.tsx index 891f467cfa..2539b74bf7 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -1,17 +1,15 @@ import { Button, Card } from '@webkom/lego-bricks'; -import { usePreparedEffect } from '@webkom/react-prepare'; -import qs from 'qs'; +import moment from 'moment'; +import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; -import { Link, useHistory, useLocation } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; -import type { ListLendableObject } from 'app/store/models/LendableObject'; import styles from './LendableObjectsList.css'; -import moment from 'moment'; -import { useState } from 'react'; -import { LendingRequest, status } from './components/LendingRequest'; +import { LendingRequest, status } from './LendingRequest'; +import type { ListLendableObject } from 'app/store/models/LendableObject'; const LendableObject = ({ lendableObject, @@ -34,32 +32,9 @@ const LendableObject = ({ ); }; -type Query = { - title?: string; -}; - -const parseQuery = (search: string): Query => { - const { title } = qs.parse(search, { - ignoreQueryPrefix: true, - }); - - return { - title: typeof title === 'string' ? title : undefined, - }; -}; - export const LendableObjectsList = () => { - const location = useLocation(); - const query = parseQuery(location.search); - const history = useHistory(); - - usePreparedEffect( - 'fetchLendableObjects', - () => { - console.log(query.title); - }, - [query.title] - ); + const [searchParams, setSearchParams] = useSearchParams(); + const [showFetchMore, setShowFetchMore] = useState(false); const lendableObjects: Array = [ { @@ -148,7 +123,6 @@ export const LendableObjectsList = () => { }, ]; - const [showFetchMore, setShowFetchMore] = useState(false); return ( @@ -177,7 +151,7 @@ export const LendableObjectsList = () => { (showFetchMore ? ( ) : ( - + ))}

Utlånsobjekter

@@ -185,22 +159,15 @@ export const LendableObjectsList = () => { className={styles.searchBar} prefix="search" placeholder="Søk etter utlånsobjekter" - onChange={(e) => { - history.replace({ - search: qs.stringify({ - ...query, - title: e.target.value ? e.target.value : undefined, - }), - }); - }} + onChange={(e) => setSearchParams(e.target.value && { search: e.target.value })} />
{lendableObjects .filter((lendableObject) => - query.title + searchParams.get("search") ? lendableObject.title .toLowerCase() - .includes(query.title.toLowerCase()) + .includes((searchParams.get("search") || "").toLowerCase()) : true ) .map((lendableObject) => ( diff --git a/app/routes/lending/LendableObjectsAdmin.css b/app/routes/lending/components/LendingAdmin.css similarity index 100% rename from app/routes/lending/LendableObjectsAdmin.css rename to app/routes/lending/components/LendingAdmin.css diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx similarity index 96% rename from app/routes/lending/LendableObjectsAdmin.tsx rename to app/routes/lending/components/LendingAdmin.tsx index 1182bb37e8..1052bc9f10 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -1,13 +1,13 @@ import { Button, Card, Flex, Icon } from '@webkom/lego-bricks'; import moment from 'moment-timezone'; +import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; -import styles from './LendableObjectsAdmin.css'; -import { useState } from 'react'; -import { ListLendableObject } from 'app/store/models/LendableObject'; -import { LendingRequest, status } from './components/LendingRequest'; +import styles from './LendingAdmin.css'; +import { LendingRequest, status } from './LendingRequest'; +import type { ListLendableObject } from 'app/store/models/LendableObject'; const LendableObjectsAdmin = () => { const lendingRequests = [ diff --git a/app/routes/lending/index.tsx b/app/routes/lending/index.tsx index 3fd6f5e276..fd7b90c415 100644 --- a/app/routes/lending/index.tsx +++ b/app/routes/lending/index.tsx @@ -1,45 +1,21 @@ -import { Route, Switch } from 'react-router-dom'; -import LendableObjectDetail from 'app/routes/lending/LendableObjectDetail'; -import LendableObjectEdit from 'app/routes/lending/LendableObjectEdit'; -import LendableObjectsList from 'app/routes/lending/LendableObjectsList'; +import { Route, Routes } from 'react-router-dom'; +import LendableObjectDetail from 'app/routes/lending/components/LendableObjectDetail'; +import LendableObjectEdit from 'app/routes/lending/components/LendableObjectEdit'; +import LendableObjectsList from 'app/routes/lending/components/LendableObjectsList'; import PageNotFound from 'app/routes/pageNotFound'; -import LendableObjectAdminDetail from './LendableObjectAdminDetail'; -import LendableObjectsAdmin from './LendableObjectsAdmin'; +import LendableObjectAdminDetail from './components/LendableObjectAdminDetail'; +import LendingAdmin from './components/LendingAdmin'; -const lendingRoute = ({ - match, -}: { - match: { - path: string; - }; -}) => ( - - - - - - - - - +const LendingRoute = () => ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + ); -export default function Lending() { - return ; -} +export default LendingRoute; diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts index 6f149c2e60..0303a8c929 100644 --- a/app/store/models/LendableObject.d.ts +++ b/app/store/models/LendableObject.d.ts @@ -1,5 +1,5 @@ -import type { RoleType } from 'app/utils/constants'; import type { EntityId } from '@reduxjs/toolkit'; +import type { RoleType } from 'app/utils/constants'; import type { Duration } from 'moment-timezone'; interface LendableObject { From 714ef1c65a8df234b015520140153eae6311f9cf Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 6 Feb 2024 21:16:28 +0100 Subject: [PATCH 15/44] Connect backend api to lendable objects --- app/reducers/lendableObjects.ts | 2 +- .../components/LendableObjectDetail.tsx | 173 ++++++++++-------- .../components/LendableObjectsList.tsx | 87 ++++----- 3 files changed, 135 insertions(+), 127 deletions(-) diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts index 33136dc8b4..ba1665a570 100644 --- a/app/reducers/lendableObjects.ts +++ b/app/reducers/lendableObjects.ts @@ -20,7 +20,7 @@ export const selectLendableObjects = createSelector( ); export const selectLendableObjectById = createSelector( (state: RootState) => state.lendableObjects.byId, - (_: RootState, id: EntityId) => id, + (_: RootState, props) => props.lendableObjectId, (lendableObjectsById, lendableObjectId) => lendableObjectsById[lendableObjectId] ); diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 3f582320e4..cf8debbc7c 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -2,7 +2,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import { Modal } from '@webkom/lego-bricks'; +import { LoadingIndicator, Modal } from '@webkom/lego-bricks'; import moment from 'moment-timezone'; import { useState } from 'react'; import { Field } from 'react-final-form'; @@ -12,8 +12,11 @@ import { Content } from 'app/components/Content'; import { Button, TextArea, TextInput } from 'app/components/Form'; import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { createValidator, required } from 'app/utils/validation'; -import type { DetailedLendableObject } from 'app/store/models/LendableObject'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { fetchLendableObject } from 'app/actions/LendableObjectActions'; type Params = { lendableObjectId: string; @@ -27,89 +30,101 @@ const LendableObjectDetail = () => { const onSubmit = () => {}; - const lendableObject: DetailedLendableObject = { - id: lendableObjectId, - title: 'Soundbox', - description: 'En soundbox som kan brukes til å spille av lyder', - lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', - image: - 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', - }; + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchLendableObject', + () => dispatch(fetchLendableObject(Number(lendableObjectId))), + [] + ); + + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, { + lendableObjectId, + }) + ); return ( - - + + {lendableObject && ( + + - - - Rediger - - + + + Rediger + + -

{lendableObject.description}

+

{lendableObject.description}

- { - setstart(info.startStr); - setend(info.endStr); - setShowLendingForm(true); - }} - /> + { + setstart(info.startStr); + setend(info.endStr); + setShowLendingForm(true); + }} + /> - setShowLendingForm(false)}> - - {({ handleSubmit }) => { - return ( - - - - - - - ); - }} - - -
+ setShowLendingForm(false)} + > + + {({ handleSubmit }) => { + return ( +
+ + + + + + ); + }} +
+
+
+ )} + ); }; diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index 2539b74bf7..f407ef29a5 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -1,12 +1,16 @@ -import { Button, Card } from '@webkom/lego-bricks'; +import { Button, Card, LoadingIndicator } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; import moment from 'moment'; import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useSearchParams } from 'react-router-dom'; +import { fetchAllLendableObjects } from 'app/actions/LendableObjectActions'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import { selectLendableObjects } from 'app/reducers/lendableObjects'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import styles from './LendableObjectsList.css'; import { LendingRequest, status } from './LendingRequest'; import type { ListLendableObject } from 'app/store/models/LendableObject'; @@ -36,35 +40,19 @@ export const LendableObjectsList = () => { const [searchParams, setSearchParams] = useSearchParams(); const [showFetchMore, setShowFetchMore] = useState(false); - const lendableObjects: Array = [ - { - id: 1, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - { - id: 2, - title: 'Noe annet enn grill', - image: - 'data:image/jpeg;base64,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', - }, - { - id: 3, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - { - id: 4, - title: 'Noe annet enn grill', - image: - 'data:image/jpeg;base64,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', - }, - { - id: 5, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - ]; + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchObjects', + () => dispatch(fetchAllLendableObjects()), + [] + ); + + const lendableObjects = useAppSelector((state) => + selectLendableObjects(state) + ); + const fetching = useAppSelector((state) => state.lendableObjects.fetching); + const myRequests = [ { id: 1, @@ -123,7 +111,6 @@ export const LendableObjectsList = () => { }, ]; - return ( @@ -151,7 +138,9 @@ export const LendableObjectsList = () => { (showFetchMore ? ( ) : ( - + ))}

Utlånsobjekter

@@ -159,23 +148,27 @@ export const LendableObjectsList = () => { className={styles.searchBar} prefix="search" placeholder="Søk etter utlånsobjekter" - onChange={(e) => setSearchParams(e.target.value && { search: e.target.value })} + onChange={(e) => + setSearchParams(e.target.value && { search: e.target.value }) + } />
- {lendableObjects - .filter((lendableObject) => - searchParams.get("search") - ? lendableObject.title - .toLowerCase() - .includes((searchParams.get("search") || "").toLowerCase()) - : true - ) - .map((lendableObject) => ( - - ))} + + {lendableObjects + .filter((lendableObjects) => + searchParams.get('search') + ? lendableObjects.title + .toLowerCase() + .includes((searchParams.get('search') || '').toLowerCase()) + : true + ) + .map((lendableObject) => ( + + ))} +
); From 14b781d1a5bebd51e79cd4a162998c8c528d7e93 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 6 Feb 2024 21:18:03 +0100 Subject: [PATCH 16/44] Fix description display --- app/routes/lending/components/LendableObjectDetail.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index cf8debbc7c..06a23807ad 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -17,6 +17,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { createValidator, required } from 'app/utils/validation'; import { usePreparedEffect } from '@webkom/react-prepare'; import { fetchLendableObject } from 'app/actions/LendableObjectActions'; +import DisplayContent from 'app/components/DisplayContent'; type Params = { lendableObjectId: string; @@ -56,7 +57,7 @@ const LendableObjectDetail = () => { -

{lendableObject.description}

+ Date: Tue, 6 Feb 2024 22:18:05 +0100 Subject: [PATCH 17/44] Add data to LendableObjectEdit --- .../lending/components/LendableObjectEdit.tsx | 95 ++++++++++++++++--- .../lending/components/LendingAdmin.tsx | 2 +- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index cd71aff084..7f6d136301 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -1,6 +1,10 @@ import { Field, FormSpy } from 'react-final-form'; -import { useParams } from 'react-router-dom'; -import { createLendableObject } from 'app/actions/LendableObjectActions'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + createLendableObject, + editLendableObject, + fetchLendableObject, +} from 'app/actions/LendableObjectActions'; import { Content } from 'app/components/Content'; import { Button, @@ -11,9 +15,14 @@ import { } from 'app/components/Form'; import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import SubmissionError from 'app/components/Form/SubmissionError'; -import { useAppDispatch } from 'app/store/hooks'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { roleOptions } from 'app/utils/constants'; import { spySubmittable } from 'app/utils/formSpyUtils'; +import { usePreparedEffect } from '@webkom/react-prepare'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { LoadingIndicator } from '@webkom/lego-bricks'; +import { fetchGroup } from 'app/actions/GroupActions'; +import { selectGroups } from 'app/reducers/groups'; type Params = { lendableObjectId: string | undefined; @@ -24,19 +33,82 @@ const LendableObjectEdit = () => { const isNew = lendableObjectId === undefined; const dispatch = useAppDispatch(); + const navigate = useNavigate(); - const onSubmit = (values) => - dispatch( - createLendableObject({ - ...values, - responsibleGroups: values.responsibleGroups.map((group) => group.id), - responsibleRoles: values.responsibleRoles.map((role) => role.value), - }) + if (!isNew) { + usePreparedEffect( + 'fetchLendableObject', + () => dispatch(fetchLendableObject(Number(lendableObjectId))), + [] ); + } + + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, { + lendableObjectId, + }) + ); + + const groups = useAppSelector((state) => selectGroups(state)); + + const onSubmit = (values) => { + 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({ + id: lendableObjectId, + ...values, + responsibleGroups: values.responsibleGroups.map((group) => group.id), + responsibleRoles: values.responsibleRoles.map((role) => role.value), + }) + ).then(() => navigate(`/lending/${lendableObjectId}`)); + } + }; + + if (!isNew && !lendableObject) { + return ( + + + + ); + } + + /*for (let group of lendableObject.responsibleGroups) { + dispatch(fetchGroup(group)); + }*/ + + if (!isNew && !groups) { + return ( + + + + ); + } + + const initialValues = !isNew + ? { + ...lendableObject, + responsibleRoles: lendableObject.responsibleRoles.map((role) => ({ + label: roleOptions.find((r) => r.value === role)?.label || role, + value: role, + })), + } + : {}; return ( - + {({ handleSubmit }) => (
{ name="description" label="Beskrivelse" component={EditorField.Field} - initialized={true} /> {

Utlånsobjekter

- + From 6c79a4f1625b2c0dbd7837ed55edbe2a1048f619 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Tue, 6 Feb 2024 22:25:21 +0100 Subject: [PATCH 18/44] Change cards to links and update routing --- .../components/LendableObjectsList.tsx | 47 ++-- .../lending/components/LendingAdmin.css | 35 ++- .../lending/components/LendingAdmin.tsx | 74 +++--- .../lending/components/LendingRequest.tsx | 216 +++++++++++------- .../components/LendingRequestAdmin.tsx | 180 +++++++++++++++ .../{LendingRequest.css => RequestItem.css} | 23 +- app/routes/lending/components/RequestItem.tsx | 70 ++++++ app/routes/lending/index.tsx | 9 +- 8 files changed, 495 insertions(+), 159 deletions(-) create mode 100644 app/routes/lending/components/LendingRequestAdmin.tsx rename app/routes/lending/components/{LendingRequest.css => RequestItem.css} (58%) create mode 100644 app/routes/lending/components/RequestItem.tsx diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index 2539b74bf7..a996c7845b 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -1,4 +1,5 @@ import { Button, Card } from '@webkom/lego-bricks'; + import moment from 'moment'; import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; @@ -8,7 +9,7 @@ import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import styles from './LendableObjectsList.css'; -import { LendingRequest, status } from './LendingRequest'; +import RequestItem, { LendingRequestStatus } from './RequestItem'; import type { ListLendableObject } from 'app/store/models/LendableObject'; const LendableObject = ({ @@ -34,7 +35,7 @@ const LendableObject = ({ export const LendableObjectsList = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [showFetchMore, setShowFetchMore] = useState(false); + const [showOldRequests, setShowOldRequests] = useState(false); const lendableObjects: Array = [ { @@ -72,7 +73,7 @@ export const LendableObjectsList = () => { startTime: moment().subtract({ hours: 2 }), endTime: moment(), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - status: status.PENDING, + status: LendingRequestStatus.PENDING, lendableObject: { id: 1, title: 'Grill', @@ -86,7 +87,7 @@ export const LendableObjectsList = () => { endTime: moment().subtract({ days: 1 }), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, - status: status.DENIED, + status: LendingRequestStatus.DENIED, lendableObject: { id: 2, title: 'Grill', @@ -100,7 +101,7 @@ export const LendableObjectsList = () => { endTime: moment().add({ hours: 4 }), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, - status: status.APPROVED, + status: LendingRequestStatus.APPROVED, lendableObject: { id: 2, title: 'Grill', @@ -108,13 +109,13 @@ export const LendableObjectsList = () => { }, }, { - id: 3, + id: 4, user: 'Test Testesen', startTime: moment().add({ hours: 2 }), endTime: moment().add({ hours: 4 }), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, - status: status.DENIED, + status: LendingRequestStatus.DENIED, lendableObject: { id: 2, title: 'Grill', @@ -123,7 +124,6 @@ export const LendableObjectsList = () => { }, ]; - return ( @@ -137,37 +137,42 @@ export const LendableObjectsList = () => {

Her var det tomt!

) : ( myRequests - .filter((request) => - request.endTime.isAfter(moment().startOf('day')) + .sort((a, b) => b.endTime.diff(a.endTime)) + .filter( + (req) => + showOldRequests || req.endTime.isAfter(moment().startOf('day')) ) - // sorting? .map((request) => ( - + )) )}
- {myRequests.length !== 0 && - (showFetchMore ? ( - - ) : ( - - ))} + {myRequests.length !== 0 && ( + + )}

Utlånsobjekter

setSearchParams(e.target.value && { search: e.target.value })} + onChange={(e) => + setSearchParams(e.target.value && { search: e.target.value }) + } />
{lendableObjects .filter((lendableObject) => - searchParams.get("search") + searchParams.get('search') ? lendableObject.title .toLowerCase() - .includes((searchParams.get("search") || "").toLowerCase()) + .includes((searchParams.get('search') || '').toLowerCase()) : true ) .map((lendableObject) => ( diff --git a/app/routes/lending/components/LendingAdmin.css b/app/routes/lending/components/LendingAdmin.css index 3b3a8597a0..790980588c 100644 --- a/app/routes/lending/components/LendingAdmin.css +++ b/app/routes/lending/components/LendingAdmin.css @@ -7,25 +7,6 @@ margin-bottom: 20px; } -.lendingRequest { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 15px; - border-radius: var(--border-radius-lg); - color: var(--lego-font-color); - transition: background-color var(--easing-fast); -} - -.notPending { - cursor: pointer; -} - -.navLink { - text-decoration: none; -} - .newLendableObject { display: flex; align-items: center; @@ -42,8 +23,24 @@ } } +.lendableObjectsContainer { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 2rem; +} + .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; + padding: 0; +} diff --git a/app/routes/lending/components/LendingAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx index 1052bc9f10..95b6eab8f8 100644 --- a/app/routes/lending/components/LendingAdmin.tsx +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -6,7 +6,7 @@ import { Link } from 'react-router-dom'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; import styles from './LendingAdmin.css'; -import { LendingRequest, status } from './LendingRequest'; +import { RequestItem, LendingRequestStatus } from './RequestItem'; import type { ListLendableObject } from 'app/store/models/LendableObject'; const LendableObjectsAdmin = () => { @@ -17,7 +17,7 @@ const LendableObjectsAdmin = () => { startTime: moment().subtract({ hours: 2 }), endTime: moment(), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - status: status.PENDING, + status: LendingRequestStatus.PENDING, lendableObject: { id: 1, title: 'Grill', @@ -31,7 +31,7 @@ const LendableObjectsAdmin = () => { endTime: moment(), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, - status: status.PENDING, + status: LendingRequestStatus.PENDING, lendableObject: { id: 2, title: 'Grill', @@ -45,7 +45,7 @@ const LendableObjectsAdmin = () => { endTime: moment(), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, - status: status.APPROVED, + status: LendingRequestStatus.APPROVED, lendableObject: { id: 2, title: 'Grill', @@ -59,7 +59,7 @@ const LendableObjectsAdmin = () => { endTime: moment(), message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', approved: false, - status: status.DENIED, + status: LendingRequestStatus.DENIED, lendableObject: { id: 2, title: 'Grill', @@ -96,7 +96,7 @@ const LendableObjectsAdmin = () => { }, ]; - const [showFetchMore, setShowFetchMore] = useState(true); + const [showOldRequests, setShowOldRequests] = useState(false); return ( @@ -111,21 +111,26 @@ const LendableObjectsAdmin = () => {

Ventende utlånsforespørsler

{lendingRequests - .filter((request) => request.status === status.PENDING) + .filter((request) => request.status === LendingRequestStatus.PENDING) .map((request) => ( - + ))} - -

Tidligere utlånsforespørsler

- - {lendingRequests - .filter((request) => request.status !== status.PENDING) - .map((request) => ( - - ))} - {showFetchMore && } - + + {showOldRequests ? ( + <> +

Tidligere utlånsforespørsler

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

Utlånsobjekter

@@ -134,23 +139,24 @@ const LendableObjectsAdmin = () => { - {lendableObjects.map((lendableObject) => ( - -

- {lendableObject.id} - {lendableObject.title} -

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

+ {lendableObject.id} - {lendableObject.title} +

+
- - ))} + ))} +
); diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 41f84ee5dd..4086fa1c43 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -1,90 +1,148 @@ -import { Button, Card, Flex, Icon } from '@webkom/lego-bricks'; -import cx from 'classnames'; -import { useState } from 'react'; -import styles from './LendingRequest.css'; - -export enum status { - PENDING, - APPROVED, - DENIED, +import moment from 'moment'; +import { Helmet } from 'react-helmet-async'; +import { Link, useParams } from 'react-router-dom'; +import { Content, ContentMain, ContentSection, ContentSidebar } from 'app/components/Content'; +import InfoList from 'app/components/InfoList'; +import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import { FromToTime } from 'app/components/Time'; +import { LendingRequestStatus } from './RequestItem'; +import type { DetailedLendableObject } from 'app/store/models/LendableObject'; + +type Params = { + requestId: string; } -const ApprovedFlag = () => { - return ( -
- - Godkjent! -
- ); -}; +const LendingRequest = () => { -const PendingFlag = () => { - return ( -
- - Venter på svar -
- ); -}; + const { requestId } = useParams(); -const DeniedFlag = () => { - return ( -
- - Avslått -
- ); -}; + const lendableObject: DetailedLendableObject = { + id: 1, + title: 'Soundbox', + description: 'En soundbox som kan brukes til å spille av lyder', + lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', + image: + 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', + }; -type LendingRequestProps = { - request: any; - isAdmin?: boolean; -}; -export const LendingRequest = ({ - request, - isAdmin = false, -}: LendingRequestProps) => { - const [isOpen, setIsOpen] = useState(false); + const request = { + id: 1, + user: { + username: "PeterTesterIProd", + fullName: "Peter TesterIProd" + }, + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.PENDING, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + } + + const otherLoans = [ + { + id: 2, + startTime: moment().subtract({ days: 1, hours: 2 }), + endTime: moment().subtract({ hours: 8 }), + }, + { + id: 3, + startTime: moment().subtract({ hours: 6 }), + endTime: moment().subtract({ hours: 2 }), + }, + ]; + + const requestEvent = { + id: String(request.id), + title: request.user.fullName, + start: request.startTime.toISOString(), + end: request.endTime.toISOString(), + backgroundColor: '#e11617', + borderColor: '#e11617', + }; + + const otherLoanEvents = otherLoans.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#999999', + borderColor: '#999999', + })); + + const otherLoanRequests = [ + { + id: 5, + startTime: moment().subtract({ hours: 2 }), + endTime: moment().add({ hours: 2 }), + }, + ]; + + const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#f57676', + borderColor: '#f57676', + })); + + const infoItems = [ + { + key: 'Status', + value: request.status, + }, + { + key: 'Tidspenn', + value: , + }, + { + key: 'Bruker', + value: {request.user.fullName}, + } + ]; return ( - setIsOpen(!isOpen)} - > - -

{request.lendableObject.title}

- {isOpen && ( - <> -

{request.message}

-

- {request.startTime.format('DD.MM.YYYY HH:mm')} -{' '} - {request.endTime.format('DD.MM.YYYY HH:mm')} -

- {request.status === status.PENDING && ( - - )} - - )} -
- - {request.status === status.APPROVED ? ( - - ) : request.status === status.DENIED ? ( - - ) : isAdmin ? ( - - - - - ) : ( - - )} - -
+ + + + + Admin + + + + + +
+

Beskjed:

+ {request.message} +
+
+ + + +
+ + {/* + + + + */} + +
); }; diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx new file mode 100644 index 0000000000..dcbd982d06 --- /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 moment from 'moment'; +import { Helmet } from 'react-helmet-async'; +import { Link, useParams } from 'react-router-dom'; +import { Content, ContentMain, ContentSection, ContentSidebar } from 'app/components/Content'; +import InfoList from 'app/components/InfoList'; +import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import { FromToTime } from 'app/components/Time'; +import { LendingRequestStatus } from './RequestItem'; +import type { DetailedLendableObject } from 'app/store/models/LendableObject'; +// import { CommentView } from 'app/components/Comments'; + +type Params = { + requestId: string; +} + +const LendingRequestAdmin = () => { + + const { requestId } = useParams(); + + const lendableObject: DetailedLendableObject = { + id: 1, + title: 'Soundbox', + description: 'En soundbox som kan brukes til å spille av lyder', + lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', + image: + 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', + }; + + + const request = { + id: 1, + user: { + username: "PeterTesterIProd", + fullName: "Peter TesterIProd" + }, + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.PENDING, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + } + + const otherLoans = [ + { + id: 2, + startTime: moment().subtract({ days: 1, hours: 2 }), + endTime: moment().subtract({ hours: 8 }), + }, + { + id: 3, + startTime: moment().subtract({ hours: 6 }), + endTime: moment().subtract({ hours: 2 }), + }, + ]; + + const requestEvent = { + id: String(request.id), + title: request.user.fullName, + start: request.startTime.toISOString(), + end: request.endTime.toISOString(), + backgroundColor: '#e11617', + borderColor: '#e11617', + }; + + const otherLoanEvents = otherLoans.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#999999', + borderColor: '#999999', + })); + + const otherLoanRequests = [ + { + id: 5, + startTime: moment().subtract({ hours: 2 }), + endTime: moment().add({ hours: 2 }), + }, + ]; + + const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#f57676', + borderColor: '#f57676', + })); + + const infoItems = [ + { + key: 'Status', + value: request.status, + }, + { + key: 'Tidspenn', + value: , + }, + { + key: 'Bruker', + value: {request.user.fullName}, + } + ]; + + return ( + + + + + Admin + + + + + +
+

Beskjed:

+ {request.message} +
+
+ + + +
+ + + + + + + + + {/* + + + + */} + +
+ ); +}; + +export default LendingRequestAdmin; diff --git a/app/routes/lending/components/LendingRequest.css b/app/routes/lending/components/RequestItem.css similarity index 58% rename from app/routes/lending/components/LendingRequest.css rename to app/routes/lending/components/RequestItem.css index ff187afd75..c0e0c570ff 100644 --- a/app/routes/lending/components/LendingRequest.css +++ b/app/routes/lending/components/RequestItem.css @@ -1,14 +1,27 @@ -@import url('~app/styles/variables.css'); - -.request { +/* same as joblistings and interestgroup, maybe own component for hoverable link container? */ +.requestItem { display: flex; align-items: center; justify-content: space-between; - cursor: pointer; + 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 auto; + margin: 0; display: flex; text-transform: uppercase; padding: 0.375rem 0.5rem; diff --git a/app/routes/lending/components/RequestItem.tsx b/app/routes/lending/components/RequestItem.tsx new file mode 100644 index 0000000000..7545c05d70 --- /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 { Link } from 'react-router-dom'; +import styles from './RequestItem.css'; + +export enum LendingRequestStatus { + PENDING, + APPROVED, + DENIED, +} + +const ApprovedFlag = () => { + return ( +
+ + Godkjent! +
+ ); +}; + +const PendingFlag = () => { + return ( +
+ + Venter på svar +
+ ); +}; + +const DeniedFlag = () => { + return ( +
+ + Avslått +
+ ); +}; + +type RequestItemProps = { + key: number; + request: any; + isAdmin?: boolean; +}; + +export const RequestItem = ({ key, request, isAdmin }: RequestItemProps) => { + return ( + + +

{request.lendableObject.title}

+ +

{request.user}

+

+ {request.startTime.format('DD.MM.YYYY HH:mm')} -{' '} + {request.endTime.format('DD.MM.YYYY HH:mm')} +

+
+
+ + {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 index fd7b90c415..853804c27d 100644 --- a/app/routes/lending/index.tsx +++ b/app/routes/lending/index.tsx @@ -5,6 +5,8 @@ import LendableObjectsList from 'app/routes/lending/components/LendableObjectsLi import PageNotFound from 'app/routes/pageNotFound'; import LendableObjectAdminDetail from './components/LendableObjectAdminDetail'; import LendingAdmin from './components/LendingAdmin'; +import LendingRequest from './components/LendingRequest'; +import LendingRequestAdmin from './components/LendingRequestAdmin'; const LendingRoute = () => ( @@ -12,8 +14,13 @@ const LendingRoute = () => ( } /> } /> } /> + } + /> + } /> + } /> } /> - } /> } /> ); From 20ce046bed2eefe66d0efaaaed3693cb4bb66794 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Thu, 15 Feb 2024 21:13:38 +0100 Subject: [PATCH 19/44] Add groups to editor --- app/reducers/lendableObjects.ts | 18 ++++++++++++++++-- .../lending/components/LendableObjectEdit.tsx | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts index ba1665a570..f57d65a25d 100644 --- a/app/reducers/lendableObjects.ts +++ b/app/reducers/lendableObjects.ts @@ -20,7 +20,21 @@ export const selectLendableObjects = createSelector( ); export const selectLendableObjectById = createSelector( (state: RootState) => state.lendableObjects.byId, + (state) => state.groups.byId, (_: RootState, props) => props.lendableObjectId, - (lendableObjectsById, lendableObjectId) => - lendableObjectsById[lendableObjectId] + (lendableObjectsById, groupsById, lendableObjectId) => { + const lendableObject = lendableObjectsById[lendableObjectId]; + + if (!lendableObject) { + return { + responsibleGroups: [], + } + } + return { + ...lendableObject, + responsibleGroups: lendableObject.responsibleGroups.map( + (groupId) => groupsById[groupId] + ), + } + } ); diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 7f6d136301..39e302db82 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -99,6 +99,10 @@ const LendableObjectEdit = () => { label: roleOptions.find((r) => r.value === role)?.label || role, value: role, })), + responsibleGroups: (lendableObject?.responsibleGroups || []).filter(Boolean).map((groups) => ({ + label: groups.name, + value: groups.id, + })), } : {}; From 1d05a512d1239838b829f4ef228c7c35706a17df Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Thu, 15 Feb 2024 21:28:43 +0100 Subject: [PATCH 20/44] Fix lendable object editing --- app/actions/LendableObjectActions.ts | 2 +- app/reducers/lendableObjects.ts | 1 + app/routes/lending/components/LendableObjectEdit.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts index 36bf9a6a58..1f82b7f76a 100644 --- a/app/actions/LendableObjectActions.ts +++ b/app/actions/LendableObjectActions.ts @@ -65,7 +65,7 @@ export function editLendableObject({ return callAPI({ types: LendableObject.EDIT, endpoint: `/lendableobject/${id}/`, - method: 'PUT', + method: 'PATCH', body: data, meta: { errorMessage: 'Endring av utlånsobjekt feilet', diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts index f57d65a25d..394614281f 100644 --- a/app/reducers/lendableObjects.ts +++ b/app/reducers/lendableObjects.ts @@ -28,6 +28,7 @@ export const selectLendableObjectById = createSelector( if (!lendableObject) { return { responsibleGroups: [], + responsibleRoles: [], } } return { diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 39e302db82..f79f174efe 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -65,7 +65,7 @@ const LendableObjectEdit = () => { editLendableObject({ id: lendableObjectId, ...values, - responsibleGroups: values.responsibleGroups.map((group) => group.id), + responsibleGroups: values.responsibleGroups.map((group) => group.id || group.value), responsibleRoles: values.responsibleRoles.map((role) => role.value), }) ).then(() => navigate(`/lending/${lendableObjectId}`)); From e2602186f39e07b4b8cf8ec7c84059974fb942cf Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Thu, 15 Feb 2024 21:42:12 +0100 Subject: [PATCH 21/44] Add lendable object deletion --- .../lending/components/LendableObjectEdit.tsx | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index f79f174efe..d81159aede 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -2,6 +2,7 @@ import { Field, FormSpy } from 'react-final-form'; import { useNavigate, useParams } from 'react-router-dom'; import { createLendableObject, + deleteLendableObject, editLendableObject, fetchLendableObject, } from 'app/actions/LendableObjectActions'; @@ -20,9 +21,10 @@ import { roleOptions } from 'app/utils/constants'; import { spySubmittable } from 'app/utils/formSpyUtils'; import { usePreparedEffect } from '@webkom/react-prepare'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; -import { LoadingIndicator } from '@webkom/lego-bricks'; -import { fetchGroup } from 'app/actions/GroupActions'; +import { ConfirmModal, Icon, LoadingIndicator } from '@webkom/lego-bricks'; +import { fetchGroup, removeMember } from 'app/actions/GroupActions'; import { selectGroups } from 'app/reducers/groups'; +import SubmitButton from 'app/components/Form/SubmitButton'; type Params = { lendableObjectId: string | undefined; @@ -65,13 +67,20 @@ const LendableObjectEdit = () => { editLendableObject({ id: lendableObjectId, ...values, - responsibleGroups: values.responsibleGroups.map((group) => group.id || group.value), + responsibleGroups: values.responsibleGroups.map( + (group) => group.id || group.value + ), responsibleRoles: values.responsibleRoles.map((role) => role.value), }) ).then(() => navigate(`/lending/${lendableObjectId}`)); } }; + const onDelete = () => { + dispatch(deleteLendableObject(Number(lendableObjectId))); + navigate('/lending'); + }; + if (!isNew && !lendableObject) { return ( @@ -99,10 +108,12 @@ const LendableObjectEdit = () => { label: roleOptions.find((r) => r.value === role)?.label || role, value: role, })), - responsibleGroups: (lendableObject?.responsibleGroups || []).filter(Boolean).map((groups) => ({ - label: groups.name, - value: groups.id, - })), + responsibleGroups: (lendableObject?.responsibleGroups || []) + .filter(Boolean) + .map((groups) => ({ + label: groups.name, + value: groups.id, + })), } : {}; @@ -149,11 +160,26 @@ const LendableObjectEdit = () => { component={TextInput.Field} /> - {spySubmittable((submittable) => ( - - ))} +
+ {spySubmittable((submittable) => ( + + ))} + {!isNew && ( + + {({ openConfirmModal }) => ( + + )} + + )} +
)} From 4122d2d4bb8e7da214bb259b5eda7a6a52c39a82 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Thu, 15 Feb 2024 22:13:04 +0100 Subject: [PATCH 22/44] Move temporary data to own fixtures file --- app/components/Search/utils.tsx | 1 + .../components/LendableObjectAdminDetail.tsx | 86 +----- .../components/LendableObjectDetail.tsx | 8 +- .../lending/components/LendableObjectEdit.tsx | 12 +- .../components/LendableObjectsList.tsx | 70 +---- .../lending/components/LendingAdmin.tsx | 117 ++------ .../lending/components/LendingRequest.tsx | 102 ++----- .../components/LendingRequestAdmin.tsx | 79 +++--- app/routes/lending/components/RequestItem.tsx | 27 +- app/routes/lending/components/fixtures.ts | 254 ++++++++++++++++++ app/store/models/LendingRequest.ts | 19 ++ 11 files changed, 386 insertions(+), 389 deletions(-) create mode 100644 app/routes/lending/components/fixtures.ts create mode 100644 app/store/models/LendingRequest.ts diff --git a/app/components/Search/utils.tsx b/app/components/Search/utils.tsx index f8ffe86777..14053e8735 100644 --- a/app/components/Search/utils.tsx +++ b/app/components/Search/utils.tsx @@ -101,6 +101,7 @@ const LINKS: Array = [ }, { key: 'lending', + requireLogin: true, title: 'Utlån', icon: 'cart-outline', url: '/lending', diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index c7f5686ad4..953eb881d1 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -2,89 +2,13 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import moment from 'moment-timezone'; import { Helmet } from 'react-helmet-async'; -import { useParams, Link } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; -import type { DetailedLendableObject } from 'app/store/models/LendableObject'; - -type Params = { - lendableObjectId: string; -}; +import { lendableObject, otherLoanEvents, otherLoanRequestEvents, request, requestEvent } from './fixtures'; const LendableObjectAdminDetail = () => { - const { lendableObjectId } = useParams(); - - const lendingRequest = { - id: 1, - user: { - id: 1, - username: 'Eik', - fullName: 'Test Testesen', - }, - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - approved: false, - }; - - const lendableObject: DetailedLendableObject = { - id: lendableObjectId, - title: 'Soundbox', - description: 'En soundbox som kan brukes til å spille av lyder', - lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', - image: - 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', - }; - - const otherLoans = [ - { - id: 2, - startTime: moment().subtract({ days: 1, hours: 2 }), - endTime: moment().subtract({ hours: 8 }), - }, - { - id: 3, - startTime: moment().subtract({ hours: 6 }), - endTime: moment().subtract({ hours: 2 }), - }, - ]; - - const requestEvent = { - id: String(lendingRequest.id), - title: lendingRequest.user.fullName, - start: lendingRequest.startTime.toISOString(), - end: lendingRequest.endTime.toISOString(), - backgroundColor: '#e11617', - borderColor: '#e11617', - }; - - const otherLoanEvents = otherLoans.map((loan) => ({ - id: String(loan.id), - title: 'Test', - start: loan.startTime.toISOString(), - end: loan.endTime.toISOString(), - backgroundColor: '#999999', - borderColor: '#999999', - })); - - const otherLoanRequests = [ - { - id: 5, - startTime: moment().subtract({ hours: 2 }), - endTime: moment().add({ hours: 2 }), - }, - ]; - - const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ - id: String(loan.id), - title: 'Test', - start: loan.startTime.toISOString(), - end: loan.endTime.toISOString(), - backgroundColor: '#f57676', - borderColor: '#f57676', - })); return ( @@ -92,9 +16,9 @@ const LendableObjectAdminDetail = () => {

- {lendingRequest.message} -{' '} - - {lendingRequest.user.fullName} + {request.message} -{' '} + + {request.user.fullName} {' '}

diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 06a23807ad..516be2c85b 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -3,21 +3,21 @@ import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; import { LoadingIndicator, Modal } 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 { useParams } from 'react-router-dom'; +import { fetchLendableObject } from 'app/actions/LendableObjectActions'; import { Content } from 'app/components/Content'; +import DisplayContent from 'app/components/DisplayContent'; import { Button, TextArea, TextInput } from 'app/components/Form'; import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { createValidator, required } from 'app/utils/validation'; -import { usePreparedEffect } from '@webkom/react-prepare'; -import { fetchLendableObject } from 'app/actions/LendableObjectActions'; -import DisplayContent from 'app/components/DisplayContent'; type Params = { lendableObjectId: string; @@ -57,7 +57,7 @@ const LendableObjectDetail = () => {
- + { ); const fetching = useAppSelector((state) => state.lendableObjects.fetching); - const myRequests = [ - { - id: 1, - user: 'Test Testesen', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - status: LendingRequestStatus.PENDING, - lendableObject: { - id: 1, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - { - id: 2, - user: 'Test Testesen', - startTime: moment().subtract({ days: 2 }), - endTime: moment().subtract({ days: 1 }), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: LendingRequestStatus.DENIED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - { - id: 3, - user: 'Test Testesen', - startTime: moment().add({ hours: 2 }), - endTime: moment().add({ hours: 4 }), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: LendingRequestStatus.APPROVED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - { - id: 4, - user: 'Test Testesen', - startTime: moment().add({ hours: 2 }), - endTime: moment().add({ hours: 4 }), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: LendingRequestStatus.DENIED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - ]; - return ( @@ -120,10 +63,10 @@ export const LendableObjectsList = () => {

Mine forespørsler

- {myRequests.length === 0 ? ( + {exampleRequests.length === 0 ? (

Her var det tomt!

) : ( - myRequests + exampleRequests .sort((a, b) => b.endTime.diff(a.endTime)) .filter( (req) => @@ -135,12 +78,11 @@ export const LendableObjectsList = () => { )}
- {myRequests.length !== 0 && ( + {exampleRequests.length !== 0 && ( )} diff --git a/app/routes/lending/components/LendingAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx index 419a656fce..46b7513fc4 100644 --- a/app/routes/lending/components/LendingAdmin.tsx +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -1,101 +1,15 @@ import { Button, Card, Flex, Icon } from '@webkom/lego-bricks'; -import moment from 'moment-timezone'; import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; +import { LendingRequestStatus } from 'app/store/models/LendingRequest'; import styles from './LendingAdmin.css'; -import { RequestItem, LendingRequestStatus } from './RequestItem'; -import type { ListLendableObject } from 'app/store/models/LendableObject'; +import { RequestItem } from './RequestItem'; +import { exampleListLendableObjects, exampleRequests } from './fixtures'; const LendableObjectsAdmin = () => { - const lendingRequests = [ - { - id: 1, - user: 'Test Testesen', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - status: LendingRequestStatus.PENDING, - lendableObject: { - id: 1, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - { - id: 2, - user: 'Test Testesen', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: LendingRequestStatus.PENDING, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - { - id: 3, - user: 'Test Testesen', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: LendingRequestStatus.APPROVED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - { - id: 4, - user: 'Test Testesen', - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - approved: false, - status: LendingRequestStatus.DENIED, - lendableObject: { - id: 2, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - }, - ]; - - const lendableObjects: Array = [ - { - id: 1, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - { - id: 2, - title: 'Soundboks', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - { - id: 3, - title: 'Soundboks2', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - { - id: 4, - title: 'Prinsessekjole', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - { - id: 5, - title: 'Falk', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - ]; - const [showOldRequests, setShowOldRequests] = useState(false); return ( @@ -110,27 +24,34 @@ const LendableObjectsAdmin = () => { />

Ventende utlånsforespørsler

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

Tidligere utlånsforespørsler

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

Utlånsobjekter

@@ -140,8 +61,8 @@ const LendableObjectsAdmin = () => {
- {lendableObjects.map((lendableObject) => ( - ( + diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 4086fa1c43..1fa34721aa 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -1,96 +1,23 @@ -import moment from 'moment'; import { Helmet } from 'react-helmet-async'; import { Link, useParams } from 'react-router-dom'; -import { Content, ContentMain, ContentSection, ContentSidebar } from 'app/components/Content'; +import { + Content, + ContentMain, + ContentSection, + ContentSidebar, +} from 'app/components/Content'; import InfoList from 'app/components/InfoList'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { FromToTime } from 'app/components/Time'; -import { LendingRequestStatus } from './RequestItem'; -import type { DetailedLendableObject } from 'app/store/models/LendableObject'; +import { lendableObject, request } from './fixtures'; type Params = { requestId: string; -} +}; const LendingRequest = () => { - const { requestId } = useParams(); - const lendableObject: DetailedLendableObject = { - id: 1, - title: 'Soundbox', - description: 'En soundbox som kan brukes til å spille av lyder', - lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', - image: - 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', - }; - - - const request = { - id: 1, - user: { - username: "PeterTesterIProd", - fullName: "Peter TesterIProd" - }, - startTime: moment().subtract({ hours: 2 }), - endTime: moment(), - message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', - status: LendingRequestStatus.PENDING, - lendableObject: { - id: 1, - title: 'Grill', - image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', - }, - } - - const otherLoans = [ - { - id: 2, - startTime: moment().subtract({ days: 1, hours: 2 }), - endTime: moment().subtract({ hours: 8 }), - }, - { - id: 3, - startTime: moment().subtract({ hours: 6 }), - endTime: moment().subtract({ hours: 2 }), - }, - ]; - - const requestEvent = { - id: String(request.id), - title: request.user.fullName, - start: request.startTime.toISOString(), - end: request.endTime.toISOString(), - backgroundColor: '#e11617', - borderColor: '#e11617', - }; - - const otherLoanEvents = otherLoans.map((loan) => ({ - id: String(loan.id), - title: 'Test', - start: loan.startTime.toISOString(), - end: loan.endTime.toISOString(), - backgroundColor: '#999999', - borderColor: '#999999', - })); - - const otherLoanRequests = [ - { - id: 5, - startTime: moment().subtract({ hours: 2 }), - endTime: moment().add({ hours: 2 }), - }, - ]; - - const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ - id: String(loan.id), - title: 'Test', - start: loan.startTime.toISOString(), - end: loan.endTime.toISOString(), - backgroundColor: '#f57676', - borderColor: '#f57676', - })); - const infoItems = [ { key: 'Status', @@ -102,18 +29,22 @@ const LendingRequest = () => { }, { key: 'Bruker', - value: {request.user.fullName}, - } + value: ( + + {request.user.fullName} + + ), + }, ]; return ( - @@ -141,7 +72,6 @@ const LendingRequest = () => { /> */} - ); }; diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index dcbd982d06..e4c96b9f2b 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -5,20 +5,24 @@ import timeGridPlugin from '@fullcalendar/timegrid'; import moment from 'moment'; import { Helmet } from 'react-helmet-async'; import { Link, useParams } from 'react-router-dom'; -import { Content, ContentMain, ContentSection, ContentSidebar } from 'app/components/Content'; +import { + Content, + ContentMain, + ContentSection, + ContentSidebar, +} from 'app/components/Content'; import InfoList from 'app/components/InfoList'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { FromToTime } from 'app/components/Time'; -import { LendingRequestStatus } from './RequestItem'; +import { LendingRequestStatus } from 'app/store/models/LendingRequest'; import type { DetailedLendableObject } from 'app/store/models/LendableObject'; // import { CommentView } from 'app/components/Comments'; type Params = { requestId: string; -} +}; const LendingRequestAdmin = () => { - const { requestId } = useParams(); const lendableObject: DetailedLendableObject = { @@ -30,12 +34,11 @@ const LendingRequestAdmin = () => { 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', }; - const request = { id: 1, user: { - username: "PeterTesterIProd", - fullName: "Peter TesterIProd" + username: 'PeterTesterIProd', + fullName: 'Peter TesterIProd', }, startTime: moment().subtract({ hours: 2 }), endTime: moment(), @@ -46,7 +49,7 @@ const LendingRequestAdmin = () => { title: 'Grill', image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', }, - } + }; const otherLoans = [ { @@ -107,18 +110,22 @@ const LendingRequestAdmin = () => { }, { key: 'Bruker', - value: {request.user.fullName}, - } + value: ( + + {request.user.fullName} + + ), + }, ]; return ( - @@ -137,30 +144,33 @@ const LendingRequestAdmin = () => { - + - - + @@ -172,7 +182,6 @@ const LendingRequestAdmin = () => { /> */} - ); }; diff --git a/app/routes/lending/components/RequestItem.tsx b/app/routes/lending/components/RequestItem.tsx index 7545c05d70..99d9af8a57 100644 --- a/app/routes/lending/components/RequestItem.tsx +++ b/app/routes/lending/components/RequestItem.tsx @@ -1,14 +1,9 @@ import { Flex, Icon } from '@webkom/lego-bricks'; import cx from 'classnames'; import { Link } from 'react-router-dom'; +import { LendingRequestStatus } from 'app/store/models/LendingRequest'; import styles from './RequestItem.css'; -export enum LendingRequestStatus { - PENDING, - APPROVED, - DENIED, -} - const ApprovedFlag = () => { return (
@@ -37,22 +32,26 @@ const DeniedFlag = () => { }; type RequestItemProps = { - key: number; + // key: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any request: any; isAdmin?: boolean; }; -export const RequestItem = ({ key, request, isAdmin }: RequestItemProps) => { +export const RequestItem = ({ request, isAdmin }: RequestItemProps) => { return ( - +

{request.lendableObject.title}

-

{request.user}

-

- {request.startTime.format('DD.MM.YYYY HH:mm')} -{' '} - {request.endTime.format('DD.MM.YYYY HH:mm')} -

+

{request.user.fullName}

+

+ {request.startTime.format('DD.MM.YYYY HH:mm')} -{' '} + {request.endTime.format('DD.MM.YYYY HH:mm')} +

diff --git a/app/routes/lending/components/fixtures.ts b/app/routes/lending/components/fixtures.ts new file mode 100644 index 0000000000..8e7498cecf --- /dev/null +++ b/app/routes/lending/components/fixtures.ts @@ -0,0 +1,254 @@ +import moment from 'moment'; +import { + LendingRequestStatus, + type LendingRequest, +} from 'app/store/models/LendingRequest'; +import type { DetailedLendableObject, ListLendableObject } from 'app/store/models/LendableObject'; + +/* +TODO: Remove when fixtures exist in backend +*/ + +export const exampleListLendableObjects: ListLendableObject[] = [ + { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 2, + title: 'Soundboks', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 3, + title: 'Soundboks2', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 4, + title: 'Prinsessekjole', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + { + id: 5, + title: 'Falk', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, +]; + +export const exampleDetailedLendableObjects: DetailedLendableObject[] = [ + { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + description: "En grill til å grille", + location: "A3", + hasContract: false, + maxLendingPeriod: moment.duration("1 day"), + responsibleGroups: [], + responsibleRoles: [] + + }, + { + id: 2, + title: 'Soundboks', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + description: "En soundboks til å soundbokse", + location: "A3", + hasContract: false, + maxLendingPeriod: moment.duration("1 day"), + responsibleGroups: [], + responsibleRoles: [] + }, +] + +export const exampleRequests: LendingRequest[] = [ + { + id: 1, + user: { + id: 1, + firstName: 'Test', + lastName: 'Testesen', + fullName: 'Test Testesen', + username: 'testes', + grade: { + name: 'yuh' + }, + abakusGroups: [], + gender: 'apache helicopter', + allergies: '', + profilePicture: '', + photoConsents: [] + }, + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.PENDING, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 2, + user: { + id: 1, + firstName: 'Test', + lastName: 'Testesen', + fullName: 'Test Testesen', + username: 'testes', + grade: { + name: 'yuh' + }, + abakusGroups: [], + gender: 'apache helicopter', + allergies: '', + profilePicture: '', + photoConsents: [] + }, + startTime: moment().subtract({ days: 2 }), + endTime: moment().subtract({ days: 1 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.DENIED, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 3, + user: { + id: 1, + firstName: 'Test', + lastName: 'Testesen', + fullName: 'Test Testesen', + username: 'testes', + grade: { + name: 'yuh' + }, + abakusGroups: [], + gender: 'apache helicopter', + allergies: '', + profilePicture: '', + photoConsents: [] + }, + startTime: moment().add({ hours: 2 }), + endTime: moment().add({ hours: 4 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.APPROVED, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 4, + user: { + id: 1, + firstName: 'Test', + lastName: 'Testesen', + fullName: 'Test Testesen', + username: 'testes', + grade: { + name: 'yuh' + }, + abakusGroups: [], + gender: 'apache helicopter', + allergies: '', + profilePicture: '', + photoConsents: [] + }, + startTime: moment().add({ hours: 2 }), + endTime: moment().add({ hours: 4 }), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.DENIED, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, +]; + +export const lendableObject: DetailedLendableObject = { + id: 1, + title: 'Soundbox', + description: 'En soundbox som kan brukes til å spille av lyder', + // lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', + image: + 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', + location: "someplace", + hasContract: false, + maxLendingPeriod: null, + responsibleRoles: [], + responsibleGroups: [], +}; + +export const request = { + id: 1, + user: { + username: 'PeterTesterIProd', + fullName: 'Peter TesterIProd', + }, + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + status: LendingRequestStatus.PENDING, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, +}; + +export const otherLoans = [ + { + id: 2, + startTime: moment().subtract({ days: 1, hours: 2 }), + endTime: moment().subtract({ hours: 8 }), + }, + { + id: 3, + startTime: moment().subtract({ hours: 6 }), + endTime: moment().subtract({ hours: 2 }), + }, +]; + +export const requestEvent = { + id: String(request.id), + title: request.user.fullName, + start: request.startTime.toISOString(), + end: request.endTime.toISOString(), + backgroundColor: '#e11617', + borderColor: '#e11617', +}; + +export const otherLoanEvents = otherLoans.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#999999', + borderColor: '#999999', +})); + +export const otherLoanRequests = [ + { + id: 5, + startTime: moment().subtract({ hours: 2 }), + endTime: moment().add({ hours: 2 }), + }, +]; + +export const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#f57676', + borderColor: '#f57676', +})); \ No newline at end of file diff --git a/app/store/models/LendingRequest.ts b/app/store/models/LendingRequest.ts new file mode 100644 index 0000000000..e0dbe2e710 --- /dev/null +++ b/app/store/models/LendingRequest.ts @@ -0,0 +1,19 @@ +import type { ListLendableObject } from './LendableObject'; +import type { User } from 'app/models'; +import type moment from 'moment-timezone'; + +export enum LendingRequestStatus { + PENDING = 'pending', + APPROVED = 'approved', + DENIED = 'denied', +} + +export type LendingRequest = { + id: number; + user: User; + startTime: moment.Moment; + endTime: moment.Moment; + message: string; + status: LendingRequestStatus; + lendableObject: ListLendableObject; +} From 682624504474cbc3cefbcd163e1922240c34e532 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Sat, 17 Feb 2024 19:26:03 +0100 Subject: [PATCH 23/44] Connect backend api to lendingrequests --- app/actions/ActionTypes.ts | 7 +++ app/actions/LendableObjectActions.ts | 18 ++++++- app/reducers/index.ts | 3 ++ app/reducers/lendingRequests.ts | 21 ++++++++ .../components/LendableObjectsList.tsx | 49 +++++++++++++------ .../lending/components/LendingRequest.tsx | 2 +- app/routes/lending/components/RequestItem.tsx | 7 +-- app/routes/lending/components/fixtures.ts | 44 ++++++++--------- app/store/createRootReducer.ts | 2 + app/store/models/LendingRequest.ts | 4 +- app/store/models/entities.ts | 3 ++ 11 files changed, 114 insertions(+), 46 deletions(-) create mode 100644 app/reducers/lendingRequests.ts diff --git a/app/actions/ActionTypes.ts b/app/actions/ActionTypes.ts index 0cf6662ebf..81bb00943a 100644 --- a/app/actions/ActionTypes.ts +++ b/app/actions/ActionTypes.ts @@ -328,3 +328,10 @@ export const LendableObject = { EDIT: generateStatuses('LendableObject.EDIT') as AAT, DELETE: generateStatuses('LendableObject.DELETE') as AAT, }; + +export const LendingRequest = { + FETCH: generateStatuses('LendingRequest.FETCH') as AAT, + CREATE: generateStatuses('LendingRequest.CREATE') as AAT, + EDIT: generateStatuses('LendingRequest.EDIT') as AAT, + DELETE: generateStatuses('LendingRequest.DELETE') as AAT, +}; \ No newline at end of file diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts index 1f82b7f76a..47631afb97 100644 --- a/app/actions/LendableObjectActions.ts +++ b/app/actions/LendableObjectActions.ts @@ -1,11 +1,11 @@ import callAPI from 'app/actions/callAPI'; -import { lendableObjectSchema } from 'app/reducers'; +import { lendableObjectSchema, lendingRequestSchema } from 'app/reducers'; import type { EntityType, NormalizedEntityPayload, } from 'app/store/models/entities'; import type { Thunk } from 'app/types'; -import { LendableObject } from './ActionTypes'; +import { LendableObject, LendingRequest } from './ActionTypes'; export function fetchAllLendableObjects(): Thunk< Promise> @@ -72,3 +72,17 @@ export function editLendableObject({ }, }); } + +export function fetchAllLendingRequests(): Thunk< + Promise> +> { + return callAPI({ + types: LendingRequest.FETCH, + endpoint: '/lendinginstance/', + schema: [lendingRequestSchema], + meta: { + errorMessage: 'Henting av utlånsforespørsler failet', + }, + propagateError: true, + }); +} \ No newline at end of file diff --git a/app/reducers/index.ts b/app/reducers/index.ts index b4679a0990..73ffdc93cf 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -135,3 +135,6 @@ export const followersUserSchema = new schema.Entity(followersKeyGen('user'), { export const lendableObjectSchema = new schema.Entity('lendableObjects', { responsibleGroups: [groupSchema], }); +export const lendingRequestSchema = new schema.Entity('lendingRequests', { + responsibleGroups: [groupSchema], +}) diff --git a/app/reducers/lendingRequests.ts b/app/reducers/lendingRequests.ts new file mode 100644 index 0000000000..7d83917026 --- /dev/null +++ b/app/reducers/lendingRequests.ts @@ -0,0 +1,21 @@ +import { createSelector } from "@reduxjs/toolkit"; +import { LendingRequest } from "app/actions/ActionTypes"; +import { RootState } from "app/store/createRootReducer"; +import createEntityReducer from "app/utils/createEntityReducer"; + + +export default createEntityReducer({ + key: 'lendingRequests', + types: { + fetch: LendingRequest.FETCH, + mutate: LendingRequest.CREATE, + delete: LendingRequest.DELETE, + }, + }); + + export const selectLendingRequests = createSelector( + (state: RootState) => state.lendingRequests.byId, + (state: RootState) => state.lendingRequests.items, + (lendingRequestsById, lendingRequestsIds) => + lendingRequestsIds.map((id) => lendingRequestsById[id]) + ); \ No newline at end of file diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index b3e04659d3..211a370c82 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -1,15 +1,16 @@ import { Button, Card, LoadingIndicator } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import moment from 'moment'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useSearchParams } from 'react-router-dom'; -import { fetchAllLendableObjects } from 'app/actions/LendableObjectActions'; +import { fetchAllLendableObjects, fetchAllLendingRequests } from 'app/actions/LendableObjectActions'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectLendableObjects } from 'app/reducers/lendableObjects'; +import { selectLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import styles from './LendableObjectsList.css'; import RequestItem from './RequestItem'; @@ -54,30 +55,46 @@ export const LendableObjectsList = () => { ); const fetching = useAppSelector((state) => state.lendableObjects.fetching); + usePreparedEffect( + 'fetchRequests', + () => dispatch(fetchAllLendingRequests()), + [] + ) + + const lendingRequests = useAppSelector((state) => + selectLendingRequests(state) + ); + + const fetchingRequests = useAppSelector((state) => state.lendingRequests.fetching); + return ( - Admin + Admin

Mine forespørsler

- {exampleRequests.length === 0 ? ( -

Her var det tomt!

- ) : ( - exampleRequests - .sort((a, b) => b.endTime.diff(a.endTime)) - .filter( - (req) => - showOldRequests || req.endTime.isAfter(moment().startOf('day')) - ) - .map((request) => ( - - )) - )} + + {lendingRequests.length === 0 ? ( +

Her var det tomt!

+ ) : ( + lendingRequests + // TODO: does not work atm.. + // .sort((a, b) => b.endDate.diff(a.endDate)) + // .filter( + // (req) => + // showOldRequests || req.endDate.isAfter(moment().startOf('day')) + // ) + .map((request) => ( + + )) + )} +
+ {exampleRequests.length !== 0 && ( + Send inn forespørsel ); }} diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index d17d95a676..8408911767 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -4,6 +4,8 @@ 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 { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; @@ -15,8 +17,6 @@ import styles from './LendableObjectsList.css'; import RequestItem from './RequestItem'; import { exampleRequests } from './fixtures'; import type { ListLendableObject } from 'app/store/models/LendableObject'; -import { fetchAllLendingRequests } from 'app/actions/LendingRequestActions'; -import abakus_icon from 'app/assets/icon-192x192.png'; const LendableObject = ({ lendableObject, diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 07f434314c..4791d852ec 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -1,5 +1,8 @@ +import { usePreparedEffect } from '@webkom/react-prepare'; import { Helmet } from 'react-helmet-async'; import { Link, useParams } from 'react-router-dom'; +import { fetchLendableObject } from 'app/actions/LendableObjectActions'; +import { fetchLendingRequest } from 'app/actions/LendingRequestActions'; import { Content, ContentMain, @@ -9,10 +12,10 @@ import { import InfoList from 'app/components/InfoList'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { FromToTime } from 'app/components/Time'; -import { usePreparedEffect } from '@webkom/react-prepare'; -import { fetchLendingRequest } from 'app/actions/LendingRequestActions'; +import { + selectLendingRequestById, +} from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -import { selectLendingRequestById } from 'app/reducers/lendingRequests'; type Params = { lendingRequestId: string; @@ -25,71 +28,75 @@ const LendingRequest = () => { usePreparedEffect( 'fetchRequest', () => dispatch(fetchLendingRequest(lendingRequestId)), - [] + [], ); const request = useAppSelector((state) => - selectLendingRequestById(state, { - lendingRequestId, - }) + selectLendingRequestById(state, lendingRequestId), + ); + + const requestFetching = useAppSelector( + (state) => state.lendingRequests.fetching, ); const infoItems = [ { key: 'Status', - value: request.pending ? 'Venter på svar' : 'Godkjent', + value: request?.pending ? 'Venter på svar' : 'Godkjent', }, { key: 'Tidspenn', - value: , + value: , }, { key: 'Bruker', value: ( - - {request.author?.fullName} + + {request?.author?.fullName} ), }, ]; return ( - - - - - Admin - - + + {request && ( + <> + + + + Admin + + - - -
-

Beskjed:

- {request.message} -
-
- - - -
+ + +
+

Beskjed:

+ {request.message} +
+
+ + + +
- {/* + {/* - */} + + )}
); }; diff --git a/app/routes/lending/index.tsx b/app/routes/lending/index.tsx index 5784f6a7df..c1a0419174 100644 --- a/app/routes/lending/index.tsx +++ b/app/routes/lending/index.tsx @@ -17,7 +17,7 @@ const lendingRoute: RouteObject[] = [ { path: ':lendableObjectId/edit', Component: LendableObjectEdit }, { path: 'admin/:lendableObjectId', Component: LendableObjectAdminDetail }, { path: 'admin', Component: LendingAdmin }, - { path: 'request', Component: LendingRequest }, + { path: 'request/:lendingRequestId', Component: LendingRequest }, { path: 'request/admin', Component: LendingRequestAdmin }, { path: '*', children: pageNotFound }, ] From ab2fd9dbff7c91eac2ca0f8b46f0246289ac293d Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Wed, 24 Apr 2024 09:21:53 +0200 Subject: [PATCH 32/44] Remove frontend fixtures and bug fixes --- app/actions/LendingRequestActions.ts | 4 +- .../components/LendableObjectAdminDetail.tsx | 115 +++++--- .../components/LendableObjectDetail.tsx | 2 +- .../lending/components/LendableObjectEdit.tsx | 23 +- .../components/LendableObjectsList.tsx | 15 +- .../lending/components/LendingAdmin.tsx | 55 +++- .../components/LendingRequestAdmin.tsx | 10 +- app/routes/lending/components/fixtures.ts | 255 ------------------ app/routes/lending/index.tsx | 22 +- 9 files changed, 161 insertions(+), 340 deletions(-) delete mode 100644 app/routes/lending/components/fixtures.ts diff --git a/app/actions/LendingRequestActions.ts b/app/actions/LendingRequestActions.ts index a6985c7aff..29a5ce71cd 100644 --- a/app/actions/LendingRequestActions.ts +++ b/app/actions/LendingRequestActions.ts @@ -22,7 +22,7 @@ export function fetchAllLendingRequests(): Thunk< } export function fetchLendingRequest( - id: number + id: number, ): Thunk>> { return callAPI({ types: LendingRequest.FETCH, @@ -35,7 +35,7 @@ export function fetchLendingRequest( } export function fetchLendingRequestsByLendableObjectId( - lendableObjectId: number + lendableObjectId: number, ): Thunk>> { return callAPI({ types: LendingRequest.FETCH, diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index 4c8dff5258..b00092258e 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -2,53 +2,88 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; +import { LoadingIndicator } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; import { Helmet } from 'react-helmet-async'; -import { Link } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; +import { fetchLendableObject } from 'app/actions/LendableObjectActions'; +import { fetchAllLendingRequests } from 'app/actions/LendingRequestActions'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; -import { - lendableObject, - otherLoanEvents, - otherLoanRequestEvents, - request, - requestEvent, -} from './fixtures'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; +import { selectLendingRequests } from 'app/reducers/lendingRequests'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import type { Params} from 'react-router-dom'; const LendableObjectAdminDetail = () => { + const { lendableObjectId } = useParams(); + + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchLendableObject', + () => dispatch(fetchLendableObject(Number(lendableObjectId))), + [], + ); + + const lendableObject = useAppSelector((state) => + selectLendableObjectById(state, { + lendableObjectId, + }), + ); + + usePreparedEffect( + 'fetchRequests', + () => dispatch(fetchAllLendingRequests()), + [], + ); + + const lendingRequests = useAppSelector((state) => + selectLendingRequests(state), + ); + + const fetchingRequests = useAppSelector( + (state) => state.lendingRequests.fetching, + ); + + if (!lendableObject) { + return ( + + + + ); + } + return ( - - - - -

- {request.message} -{' '} - - {request.user.fullName} - {' '} -

- - -
+ + + + + + + + ); }; export default LendableObjectAdminDetail; + diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index ea3534fe93..70059b6cdb 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -43,7 +43,7 @@ const LendableObjectDetail = () => { ...values, pending: false, lendableObject: lendableObjectId, - }) + }), ).then(() => navigate('/lending')); }; diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 43104a7106..d2f20ad449 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -14,11 +14,12 @@ import { EditorField, Form, SelectInput, + SubmitButton, TextInput, } from 'app/components/Form'; import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import SubmissionError from 'app/components/Form/SubmissionError'; -import { selectGroups } from 'app/reducers/groups'; +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'; @@ -39,17 +40,17 @@ const LendableObjectEdit = () => { usePreparedEffect( 'fetchLendableObject', () => dispatch(fetchLendableObject(Number(lendableObjectId))), - [] + [], ); } const lendableObject = useAppSelector((state) => selectLendableObjectById(state, { lendableObjectId, - }) + }), ); - const groups = useAppSelector((state) => selectGroups(state)); + const groups = useAppSelector((state) => selectAllGroups(state)); const onSubmit = (values) => { if (isNew) { @@ -58,7 +59,7 @@ const LendableObjectEdit = () => { ...values, responsibleGroups: values.responsibleGroups?.map((group) => group.id), responsibleRoles: values.responsibleRoles?.map((role) => role.value), - }) + }), ).then(() => navigate('/lending')); } else { dispatch( @@ -66,10 +67,10 @@ const LendableObjectEdit = () => { id: lendableObjectId, ...values, responsibleGroups: values.responsibleGroups.map( - (group) => group.id || group.value + (group) => group.id || group.value, ), responsibleRoles: values.responsibleRoles.map((role) => role.value), - }) + }), ).then(() => navigate(`/lending/${lendableObjectId}`)); } }; @@ -87,10 +88,6 @@ const LendableObjectEdit = () => { ); } - /*for (let group of lendableObject.responsibleGroups) { - dispatch(fetchGroup(group)); - }*/ - if (!isNew && !groups) { return ( @@ -160,9 +157,9 @@ const LendableObjectEdit = () => {
{spySubmittable((submittable) => ( - + ))} {!isNew && ( { usePreparedEffect( 'fetchObjects', () => dispatch(fetchAllLendableObjects()), - [] + [], ); const lendableObjects = useAppSelector((state) => - selectLendableObjects(state) + selectLendableObjects(state), ); const fetching = useAppSelector((state) => state.lendableObjects.fetching); usePreparedEffect( 'fetchRequests', () => dispatch(fetchAllLendingRequests()), - [] + [], ); const lendingRequests = useAppSelector((state) => - selectLendingRequests(state) + selectLendingRequests(state), ); const fetchingRequests = useAppSelector( - (state) => state.lendingRequests.fetching + (state) => state.lendingRequests.fetching, ); return ( @@ -97,7 +96,7 @@ export const LendableObjectsList = () => {
- {exampleRequests.length !== 0 && ( + {lendingRequests.length !== 0 && ( )} -

Utlånsobjekter

+

Utlånsobjekter

{ [], ); - const lendableObjects = useAppSelector((state) => - selectAllLendableObjects(state), - ); + const lendableObjects = useAppSelector(selectAllLendableObjects); const fetchingObjects = useAppSelector( (state) => state.lendableObjects.fetching, ); @@ -52,11 +50,12 @@ const LendableObjectsAdmin = () => { (state) => state.lendingRequests.fetching, ); + const title = "Utlånsforepørsler"; return ( - + { />

Ventende utlånsforespørsler

- + {lendingRequests .filter( (request) => request.status === LendingRequestStatus.PENDING, @@ -78,7 +77,7 @@ const LendableObjectsAdmin = () => { {showOldRequests ? ( <>

Tidligere utlånsforespørsler

- + {lendingRequests .filter( (request) => request.status !== LendingRequestStatus.PENDING, @@ -99,8 +98,8 @@ const LendableObjectsAdmin = () => {

Utlånsobjekter

- - + + diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index aa1c867846..255653d2de 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -42,7 +42,7 @@ const LendingRequest = () => { value: request?.pending ? 'Venter på svar' : 'Godkjent', }, { - key: 'Tidspenn', + key: 'Tidsspenn', value: , }, { diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index a2fb64452d..7497e1ada9 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -35,7 +35,7 @@ const LendingRequestAdmin = () => { usePreparedEffect( 'fetchRequest', - () => dispatch(fetchLendingRequest(Number(lendingRequestId))), + () => dispatch(fetchLendingRequest(lendingRequestId)), [], ); @@ -50,7 +50,7 @@ const LendingRequestAdmin = () => { () => { if (request && request.lendableObject?.id) { dispatch( - fetchLendingRequestsForLendableObject(request.lendableObject.id as number), + fetchLendingRequestsForLendableObject(request.lendableObject.id), ); } }, @@ -67,7 +67,7 @@ const LendingRequestAdmin = () => { const otherLoanRequests = otherRequests.filter((loan) => loan.pending); if (!request) { - return

Ukjent forespørsel

; + return

Ukjent forespørsel

; } const requestEvent = { @@ -103,7 +103,7 @@ const LendingRequestAdmin = () => { value: request.pending ? 'Venter på svar' : 'Godkjent', }, { - key: 'Tidspenn', + key: 'Tidsspenn', value: , }, { @@ -116,14 +116,15 @@ const LendingRequestAdmin = () => { }, ]; + const title = `Forespørsel om utlån av ${request.lendableObject.title}`; return ( { @@ -33,7 +33,7 @@ const DeniedFlag = () => { }; type RequestItemProps = { - request: any; + request: LendingRequest; isAdmin?: boolean; }; @@ -46,7 +46,7 @@ export const RequestItem = ({ request, isAdmin }: RequestItemProps) => {

{request.lendableObject?.title}

- +

{request.author?.fullName}

{moment(request.startDate).format('DD.MM.YYYY')} -{' '} diff --git a/app/store/models/LendingRequest.ts b/app/store/models/LendingRequest.ts index 7d00a7311b..a60b1c414a 100644 --- a/app/store/models/LendingRequest.ts +++ b/app/store/models/LendingRequest.ts @@ -10,7 +10,7 @@ export enum LendingRequestStatus { export type LendingRequest = { id: number; - user: User; + author: User; startDate: moment.Moment; endDate: moment.Moment; message: string; From 1617206c94e404121f2334694989b93368bee1fd Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Tue, 30 Apr 2024 22:16:31 +0200 Subject: [PATCH 36/44] Type LendableObjectEdit form --- app/actions/LendableObjectActions.ts | 8 ++++---- .../lending/components/LendableObjectEdit.tsx | 13 ++++++++----- app/store/models/LendableObject.d.ts | 11 +++++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts index 5705cc6b7c..7b732fc522 100644 --- a/app/actions/LendableObjectActions.ts +++ b/app/actions/LendableObjectActions.ts @@ -40,8 +40,8 @@ export function deleteLendableObject(id: EntityId) { }); } -export function createLendableObject(data: EntityId) { - return callAPI({ +export function createLendableObject(data) { + return callAPI({ types: LendableObject.CREATE, endpoint: '/lendableobject/', method: 'POST', @@ -56,8 +56,8 @@ export function createLendableObject(data: EntityId) { export function editLendableObject({ id, ...data -}: Record) { - return callAPI({ +}) { + return callAPI({ types: LendableObject.EDIT, endpoint: `/lendableobject/${id}/`, method: 'PATCH', diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 83b86539d3..0698be0eaa 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -9,6 +9,7 @@ import { fetchLendableObject, } from 'app/actions/LendableObjectActions'; import { Content } from 'app/components/Content'; +import { FlexRow } from 'app/components/FlexBox'; import { Button, EditorField, @@ -23,12 +24,14 @@ 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 { FlexRow } from 'app/components/FlexBox'; +import type { EditingLendableObject } from 'app/store/models/LendableObject'; type Params = { lendableObjectId: string | undefined; }; +const TypedLegoForm = LegoFinalForm; + const LendableObjectEdit = () => { const { lendableObjectId } = useParams(); const isNew = lendableObjectId === undefined; @@ -48,7 +51,7 @@ const LendableObjectEdit = () => { const groups = useAppSelector(selectAllGroups); - const onSubmit = (values) => { + const onSubmit = (values: EditingLendableObject) => { if (isNew) { dispatch( createLendableObject({ @@ -60,8 +63,8 @@ const LendableObjectEdit = () => { } else { dispatch( editLendableObject({ - id: lendableObjectId, ...values, + id: lendableObjectId, responsibleGroups: values.responsibleGroups.map( (group) => group.id || group.value, ), @@ -113,7 +116,7 @@ const LendableObjectEdit = () => { return ( - { )} - + ); }; diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts index 2b1760398f..4d8c2c6e7c 100644 --- a/app/store/models/LendableObject.d.ts +++ b/app/store/models/LendableObject.d.ts @@ -1,3 +1,5 @@ +import { DetailedLendableObject } from 'app/store/models/LendableObject'; +import { EditingLendableObject } from './LendableObject.d'; import type { EntityId } from '@reduxjs/toolkit'; import type { RoleType } from 'app/utils/constants'; import type { Duration } from 'moment-timezone'; @@ -27,3 +29,12 @@ export type DetailedLendableObject = ListLendableObject & >; export type UnknownLendableObject = ListLendableObject | DetailedLendableObject; + +export type EditingLendableObject = Omit< + DetailedLendableObject, + | 'responsibleRoles' + | 'responsibleGroups' + > & { + responsibleRoles: { label: string, value: RoleType}[]; + responsibleGroups: PublicGroup[]; + } \ No newline at end of file From b9a154395ff4e39f4562aabf93aeb960796c8e18 Mon Sep 17 00:00:00 2001 From: Vebjorn Elvekrok Date: Tue, 30 Apr 2024 22:19:53 +0200 Subject: [PATCH 37/44] Add skeleton, reformat with prettier --- app/actions/LendableObjectActions.ts | 10 +- app/actions/LendingRequestActions.ts | 11 +- app/reducers/index.ts | 13 +- app/reducers/lendableObjects.ts | 5 +- app/reducers/lendingRequests.ts | 4 +- .../components/LendableObjectDetail.tsx | 148 ++++++++-------- .../lending/components/LendableObjectEdit.tsx | 159 +++++++++--------- .../components/LendableObjectsList.tsx | 2 +- .../lending/components/LendingAdmin.tsx | 19 +-- .../lending/components/LendingRequest.tsx | 24 +-- .../components/LendingRequestAdmin.tsx | 117 ++++++------- app/routes/lending/components/RequestItem.tsx | 5 +- app/store/models/LendableObject.d.ts | 16 +- app/store/models/LendingRequest.ts | 1 + 14 files changed, 252 insertions(+), 282 deletions(-) diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts index 7b732fc522..88e7eb507c 100644 --- a/app/actions/LendableObjectActions.ts +++ b/app/actions/LendableObjectActions.ts @@ -2,7 +2,10 @@ 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'; +import type { + DetailedLendableObject, + ListLendableObject, +} from 'app/store/models/LendableObject'; export function fetchAllLendableObjects() { return callAPI({ @@ -53,10 +56,7 @@ export function createLendableObject(data) { }); } -export function editLendableObject({ - id, - ...data -}) { +export function editLendableObject({ id, ...data }) { return callAPI({ types: LendableObject.EDIT, endpoint: `/lendableobject/${id}/`, diff --git a/app/actions/LendingRequestActions.ts b/app/actions/LendingRequestActions.ts index 787ad03757..3f65fb5400 100644 --- a/app/actions/LendingRequestActions.ts +++ b/app/actions/LendingRequestActions.ts @@ -1,7 +1,8 @@ import { LendingRequest } from 'app/actions/ActionTypes'; import callAPI from 'app/actions/callAPI'; import { lendingRequestSchema } from 'app/reducers'; -import type { LendingRequest as LendingRequestModel } from 'app/store/models/LendingRequest.ts'; +import type { EntityId } from '@reduxjs/toolkit'; +import type { LendingRequest as LendingRequestModel } from 'app/store/models/LendingRequest'; export function fetchAllLendingRequests() { return callAPI({ @@ -15,9 +16,7 @@ export function fetchAllLendingRequests() { }); } -export function fetchLendingRequest( - id: number, -) { +export function fetchLendingRequest(id: EntityId) { return callAPI({ types: LendingRequest.FETCH, endpoint: `/lendinginstance/${id}/`, @@ -29,7 +28,7 @@ export function fetchLendingRequest( } export function fetchLendingRequestsForLendableObject( - lendableObjectId: number, + lendableObjectId: EntityId, ) { return callAPI({ types: LendingRequest.FETCH, @@ -42,7 +41,7 @@ export function fetchLendingRequestsForLendableObject( } export function createLendingRequest(data: any) { - return callAPI({ + return callAPI({ types: LendingRequest.CREATE, endpoint: '/lendinginstance/', method: 'POST', diff --git a/app/reducers/index.ts b/app/reducers/index.ts index 6b43f46c7c..dc0ff89529 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -154,7 +154,12 @@ 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); +export const lendableObjectSchema = new schema.Entity( + EntityType.LendableObjects, + { + responsibleGroups: [groupSchema], + }, +); +export const lendingRequestSchema = new schema.Entity( + EntityType.LendingRequests, +); diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts index 3313112396..e60ca33083 100644 --- a/app/reducers/lendableObjects.ts +++ b/app/reducers/lendableObjects.ts @@ -19,6 +19,5 @@ const lendableObjectsSlice = createSlice({ export default lendableObjectsSlice.reducer; export const { selectById: selectLendableObjectById, - selectAll: selectAllLendableObjects } = legoAdapter.getSelectors( - (state: RootState) => state.lendableObjects, -); + selectAll: selectAllLendableObjects, +} = legoAdapter.getSelectors((state: RootState) => state.lendableObjects); diff --git a/app/reducers/lendingRequests.ts b/app/reducers/lendingRequests.ts index dc2bf97f10..b91549de1b 100644 --- a/app/reducers/lendingRequests.ts +++ b/app/reducers/lendingRequests.ts @@ -4,7 +4,6 @@ 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({ @@ -25,4 +24,5 @@ export const { selectByField: selectLendingRequestsByField, } = legoAdapter.getSelectors((state: RootState) => state.lendingRequests); -export const selectLendingRequestsByLendableObjectId = selectLendingRequestsByField('lendableObjectId'); +export const selectLendingRequestsByLendableObjectId = + selectLendingRequestsByField('lendableObjectId'); diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index cd79c81515..0a15acbf66 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -2,7 +2,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import { LoadingIndicator, Modal } from '@webkom/lego-bricks'; +import { Modal } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import moment from 'moment-timezone'; import { useState } from 'react'; @@ -52,97 +52,93 @@ const LendableObjectDetail = () => { usePreparedEffect( 'fetchLendableObject', () => lendableObjectId && dispatch(fetchLendableObject(lendableObjectId)), - [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(), }; return ( - - {lendableObject && ( - - + + - - - Rediger - - + + + Rediger + + - + - { - setstart(info.startStr); - setend(info.endStr); - setShowLendingForm(true); - }} - /> + { + setStart(info.startStr); + setEnd(info.endStr); + setShowLendingForm(true); + }} + /> - setShowLendingForm(false)} - > - - {({ handleSubmit }) => { - return ( -

- - - - Send inn forespørsel - - ); - }} - - -
- )} -
+ setShowLendingForm(false)}> + + {({ handleSubmit }) => { + return ( +
+ + + + Send inn forespørsel + + ); + }} +
+
+
); }; diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 0698be0eaa..8fa44215f9 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -1,4 +1,4 @@ -import { LoadingIndicator, ConfirmModal, Flex } from '@webkom/lego-bricks'; +import { ConfirmModal } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import { Field } from 'react-final-form'; import { useNavigate, useParams } from 'react-router-dom'; @@ -82,102 +82,93 @@ const LendableObjectEdit = () => { navigate('/lending'); }; - if (!isNew && !lendableObject) { - return ( - - - - ); - } - - if (!isNew && !groups) { - return ( - - - - ); - } - const initialValues = !isNew ? { ...lendableObject, - responsibleRoles: lendableObject.responsibleRoles.map((role) => ({ + responsibleRoles: lendableObject?.responsibleRoles.map((role) => ({ label: roleOptions.find((r) => r.value === role)?.label || role, value: role, })), responsibleGroups: (lendableObject?.responsibleGroups || []) .filter(Boolean) - .map((groups) => ({ - label: groups.name, - value: groups.id, + .map((group) => ({ + label: groups.find((g) => g.id === group)?.name || group, + value: groups.find((g) => g.id === group), })), } : {}; + const showSkeleton = !(isNew || (lendableObject && groups)); return ( - - - {({ handleSubmit }) => ( -
- - - - - - - - - {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} - - {!isNew && ( - - {({ openConfirmModal }) => ( - - )} - - )} - - - )} -
+ + {!showSkeleton && ( + + {({ handleSubmit }) => ( +
+ + + + + + + + + {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} + + {!isNew && ( + + {({ openConfirmModal }) => ( + + )} + + )} + + + )} +
+ )}
); }; diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index a8cdf59baf..2583902544 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -100,7 +100,7 @@ export const LendableObjectsList = () => { )} -

Utlånsobjekter

+

Utlånsobjekter

{ const dispatch = useAppDispatch(); usePreparedEffect( - 'fetchObjects', - () => dispatch(fetchAllLendableObjects()), + 'fetchAllLendingObjectsAndRequests', + () => { + dispatch(fetchAllLendingRequests()); + dispatch(fetchAllLendableObjects()); + }, [], ); @@ -36,12 +39,6 @@ const LendableObjectsAdmin = () => { (state) => state.lendableObjects.fetching, ); - usePreparedEffect( - 'fetchRequests', - () => dispatch(fetchAllLendingRequests()), - [], - ); - const lendingRequests = useAppSelector((state) => selectAllLendingRequests(state), ); @@ -50,7 +47,7 @@ const LendableObjectsAdmin = () => { (state) => state.lendingRequests.fetching, ); - const title = "Utlånsforepørsler"; + const title = 'Utlånsforepørsler'; return ( @@ -97,7 +94,7 @@ const LendableObjectsAdmin = () => { )}

Utlånsobjekter

- + @@ -123,7 +120,7 @@ const LendableObjectsAdmin = () => { ))}
- + ); }; diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 255653d2de..3e1c554246 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -14,22 +14,22 @@ import { FromToTime } from 'app/components/Time'; import { selectLendingRequestById } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -type Params = { +type LendingRequestParams = { lendingRequestId: string; }; const LendingRequest = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams(); const dispatch = useAppDispatch(); usePreparedEffect( 'fetchRequest', - () => dispatch(fetchLendingRequest(Number(lendingRequestId))), + () => lendingRequestId && dispatch(fetchLendingRequest(lendingRequestId)), [], ); const request = useAppSelector((state) => - selectLendingRequestById(state, lendingRequestId), + selectLendingRequestById(state, Number(lendingRequestId)), ); const requestFetching = useAppSelector( @@ -48,20 +48,21 @@ const LendingRequest = () => { { key: 'Bruker', value: ( - + {request?.author?.fullName} ), }, ]; + const title = `Forespørsel om utlån av ${request?.lendableObject.title}`; return ( {request && ( <> - + { - - {/* - - - - */} )} diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index 7497e1ada9..8a90f45185 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -63,8 +63,8 @@ const LendingRequestAdmin = () => { }), ); - const otherLoans = otherRequests.filter((loan) => !loan.pending); - const otherLoanRequests = otherRequests.filter((loan) => loan.pending); + const otherApprovedRequests = otherRequests.filter((loan) => !loan.pending); + const otherPendingRequests = otherRequests.filter((loan) => loan.pending); if (!request) { return

Ukjent forespørsel

; @@ -79,7 +79,7 @@ const LendingRequestAdmin = () => { borderColor: '#e11617', }; - const otherLoanEvents = otherLoans.map((loan) => ({ + const otherApprovedEvents = otherApprovedRequests.map((loan) => ({ id: String(loan.id), title: request.author?.fullName, start: loan.startDate, @@ -88,7 +88,7 @@ const LendingRequestAdmin = () => { borderColor: '#999999', })); - const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ + const otherPendingEvents = otherPendingRequests.map((loan) => ({ id: String(loan.id), title: request?.author?.fullName, start: loan.startDate, @@ -120,66 +120,55 @@ const LendingRequestAdmin = () => { return ( - - - - Admin - - - - - -
-

Beskjed:

- {request.message} -
-
- - - -
- - - - - - - - {/* - - - - */} + + + + Admin + + + + + +
+

Beskjed:

+ {request.message} +
+
+ + + +
+ + + + + +
); diff --git a/app/routes/lending/components/RequestItem.tsx b/app/routes/lending/components/RequestItem.tsx index c3e41ade42..89278232a1 100644 --- a/app/routes/lending/components/RequestItem.tsx +++ b/app/routes/lending/components/RequestItem.tsx @@ -2,7 +2,10 @@ import { Flex, Icon } from '@webkom/lego-bricks'; import cx from 'classnames'; import moment from 'moment-timezone'; import { Link } from 'react-router-dom'; -import { LendingRequest, LendingRequestStatus } from 'app/store/models/LendingRequest'; +import { + LendingRequest, + LendingRequestStatus, +} from 'app/store/models/LendingRequest'; import styles from './RequestItem.css'; const ApprovedFlag = () => { diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts index 4d8c2c6e7c..2d5e09201f 100644 --- a/app/store/models/LendableObject.d.ts +++ b/app/store/models/LendableObject.d.ts @@ -1,6 +1,5 @@ -import { DetailedLendableObject } from 'app/store/models/LendableObject'; -import { EditingLendableObject } from './LendableObject.d'; 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'; @@ -31,10 +30,9 @@ export type DetailedLendableObject = ListLendableObject & export type UnknownLendableObject = ListLendableObject | DetailedLendableObject; export type EditingLendableObject = Omit< - DetailedLendableObject, - | 'responsibleRoles' - | 'responsibleGroups' - > & { - responsibleRoles: { label: string, value: RoleType}[]; - responsibleGroups: PublicGroup[]; - } \ No newline at end of file + DetailedLendableObject, + 'responsibleRoles' | 'responsibleGroups' +> & { + responsibleRoles: { label: string; value: RoleType }[]; + responsibleGroups: PublicGroup[]; +}; diff --git a/app/store/models/LendingRequest.ts b/app/store/models/LendingRequest.ts index a60b1c414a..173af4ca82 100644 --- a/app/store/models/LendingRequest.ts +++ b/app/store/models/LendingRequest.ts @@ -15,5 +15,6 @@ export type LendingRequest = { endDate: moment.Moment; message: string; status: LendingRequestStatus; + pending: boolean; lendableObject: ListLendableObject; }; From c47812ecb212fa80abd1bc035e012554a9e17850 Mon Sep 17 00:00:00 2001 From: Vebjorn Elvekrok Date: Wed, 1 May 2024 18:43:26 +0200 Subject: [PATCH 38/44] Fix comments --- app/actions/LendingRequestActions.ts | 4 +- app/components/Search/utils.tsx | 7 - .../components/LendableObjectDetail.tsx | 5 +- .../lending/components/LendableObjectEdit.tsx | 137 +++++++++--------- .../components/LendableObjectsList.tsx | 9 +- .../components/LendingRequestAdmin.tsx | 12 +- app/routes/lending/components/RequestItem.tsx | 6 +- app/store/models/LendingRequest.ts | 4 +- 8 files changed, 85 insertions(+), 99 deletions(-) diff --git a/app/actions/LendingRequestActions.ts b/app/actions/LendingRequestActions.ts index 3f65fb5400..168d041fbb 100644 --- a/app/actions/LendingRequestActions.ts +++ b/app/actions/LendingRequestActions.ts @@ -5,7 +5,7 @@ import type { EntityId } from '@reduxjs/toolkit'; import type { LendingRequest as LendingRequestModel } from 'app/store/models/LendingRequest'; export function fetchAllLendingRequests() { - return callAPI({ + return callAPI({ types: LendingRequest.FETCH, endpoint: '/lendinginstance/', schema: [lendingRequestSchema], @@ -30,7 +30,7 @@ export function fetchLendingRequest(id: EntityId) { export function fetchLendingRequestsForLendableObject( lendableObjectId: EntityId, ) { - return callAPI({ + return callAPI({ types: LendingRequest.FETCH, endpoint: `/lendableobject/${lendableObjectId}/lendinginstances/`, schema: [lendingRequestSchema], diff --git a/app/components/Search/utils.tsx b/app/components/Search/utils.tsx index 074163e1c3..91a33e845c 100644 --- a/app/components/Search/utils.tsx +++ b/app/components/Search/utils.tsx @@ -111,13 +111,6 @@ const LINKS: Array = [ icon: 'stats-chart-outline', url: '/polls', }, - { - key: 'lending', - requireLogin: true, - title: 'Utlån', - icon: 'cart-outline', - url: '/lending', - }, { admin: true, key: 'announcements', diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 0a15acbf66..e3ba918b9a 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -68,11 +68,12 @@ const LendableObjectDetail = () => { endDate: moment(end).toISOString(), }; + const title = `Utlån av ${lendableObject?.title}`; return ( - + - + Rediger diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 8fa44215f9..70bc9ff19b 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -1,4 +1,4 @@ -import { ConfirmModal } from '@webkom/lego-bricks'; +import { ConfirmModal, Flex } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import { Field } from 'react-final-form'; import { useNavigate, useParams } from 'react-router-dom'; @@ -9,7 +9,6 @@ import { fetchLendableObject, } from 'app/actions/LendableObjectActions'; import { Content } from 'app/components/Content'; -import { FlexRow } from 'app/components/FlexBox'; import { Button, EditorField, @@ -41,8 +40,8 @@ const LendableObjectEdit = () => { usePreparedEffect( 'fetchLendableObject', - () => dispatch(fetchLendableObject(Number(lendableObjectId))), - [isNew], + () => lendableObjectId && dispatch(fetchLendableObject(lendableObjectId)), + [lendableObjectId], ); const lendableObject = useAppSelector((state) => @@ -101,74 +100,68 @@ const LendableObjectEdit = () => { const showSkeleton = !(isNew || (lendableObject && groups)); return ( - {!showSkeleton && ( - - {({ handleSubmit }) => ( -
- - - - - - - - - {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} - - {!isNew && ( - - {({ openConfirmModal }) => ( - - )} - - )} - - - )} -
- )} + + {({ handleSubmit }) => ( +
+ + + + + + + + + {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} + + {!isNew && ( + + {({ openConfirmModal }) => ( + + )} + + )} + + + )} +
); }; diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index 2583902544..7a3444196d 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -1,6 +1,6 @@ import { Button, Card, LoadingIndicator } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useSearchParams } from 'react-router-dom'; import { fetchAllLendableObjects } from 'app/actions/LendableObjectActions'; @@ -54,7 +54,7 @@ export const LendableObjectsList = () => { const fetching = useAppSelector((state) => state.lendableObjects.fetching); usePreparedEffect( - 'fetchRequests', + 'fetchAllLendingRequests', () => dispatch(fetchAllLendingRequests()), [], ); @@ -65,10 +65,11 @@ export const LendableObjectsList = () => { (state) => state.lendingRequests.fetching, ); + const title = 'Utlån'; return ( - - + + Admin diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index 8a90f45185..9a22a7fdda 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -75,8 +75,8 @@ const LendingRequestAdmin = () => { title: request.author?.fullName, start: request.startDate, end: request.endDate, - backgroundColor: '#e11617', - borderColor: '#e11617', + backgroundColor: 'var(--lego-red-color)', + borderColor: 'var(--lego-red-color)', }; const otherApprovedEvents = otherApprovedRequests.map((loan) => ({ @@ -84,8 +84,8 @@ const LendingRequestAdmin = () => { title: request.author?.fullName, start: loan.startDate, end: loan.endDate, - backgroundColor: '#999999', - borderColor: '#999999', + backgroundColor: 'var(--color-gray-5)', + borderColor: 'var(--color-gray-5)', })); const otherPendingEvents = otherPendingRequests.map((loan) => ({ @@ -93,8 +93,8 @@ const LendingRequestAdmin = () => { title: request?.author?.fullName, start: loan.startDate, end: loan.endDate, - backgroundColor: '#f57676', - borderColor: '#f57676', + backgroundColor: 'var(--color-red-2)', + borderColor: 'var(--color-red-2)', })); const infoItems = [ diff --git a/app/routes/lending/components/RequestItem.tsx b/app/routes/lending/components/RequestItem.tsx index 89278232a1..86c8374909 100644 --- a/app/routes/lending/components/RequestItem.tsx +++ b/app/routes/lending/components/RequestItem.tsx @@ -2,11 +2,9 @@ import { Flex, Icon } from '@webkom/lego-bricks'; import cx from 'classnames'; import moment from 'moment-timezone'; import { Link } from 'react-router-dom'; -import { - LendingRequest, - LendingRequestStatus, -} from 'app/store/models/LendingRequest'; +import { LendingRequestStatus } from 'app/store/models/LendingRequest'; import styles from './RequestItem.css'; +import type { LendingRequest } from 'app/store/models/LendingRequest'; const ApprovedFlag = () => { return ( diff --git a/app/store/models/LendingRequest.ts b/app/store/models/LendingRequest.ts index 173af4ca82..3aae67b3be 100644 --- a/app/store/models/LendingRequest.ts +++ b/app/store/models/LendingRequest.ts @@ -1,5 +1,5 @@ import type { ListLendableObject } from './LendableObject'; -import type { User } from 'app/models'; +import type { PublicUser } from 'app/store/models/User'; import type moment from 'moment-timezone'; export enum LendingRequestStatus { @@ -10,7 +10,7 @@ export enum LendingRequestStatus { export type LendingRequest = { id: number; - author: User; + author: PublicUser; startDate: moment.Moment; endDate: moment.Moment; message: string; From e99b37842dd334c8d38f310eba8191b8d8c6f084 Mon Sep 17 00:00:00 2001 From: Vebjorn Elvekrok Date: Wed, 1 May 2024 20:11:33 +0200 Subject: [PATCH 39/44] Fix lending admin style and bugs, remove global td styling in bricks --- .../components/LendableObjectAdminDetail.tsx | 4 +- .../components/LendableObjectDetail.tsx | 2 +- .../lending/components/LendableObjectEdit.tsx | 132 +++++++++--------- .../components/LendableObjectsList.css | 1 - .../components/LendableObjectsList.tsx | 13 +- .../lending/components/LendingAdmin.css | 8 +- .../lending/components/LendingAdmin.tsx | 84 ++++++----- .../lending/components/LendingRequest.tsx | 2 +- .../components/LendingRequestAdmin.css | 5 + .../components/LendingRequestAdmin.tsx | 16 ++- app/store/models/LendingRequest.ts | 18 ++- packages/lego-bricks/package.json | 2 +- packages/lego-bricks/src/global.css | 1 - 13 files changed, 150 insertions(+), 138 deletions(-) create mode 100644 app/routes/lending/components/LendingRequestAdmin.css diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index 49aa630955..09c797c024 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -27,9 +27,7 @@ const LendableObjectAdminDetail = () => { ); const lendableObject = useAppSelector((state) => - selectLendableObjectById(state, { - lendableObjectId, - }), + selectLendableObjectById(state, lendableObjectId), ); usePreparedEffect( diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index e3ba918b9a..1a151879c7 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -130,7 +130,7 @@ const LendableObjectDetail = () => { /> Send inn forespørsel diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 70bc9ff19b..4a533621c6 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -1,4 +1,4 @@ -import { ConfirmModal, Flex } from '@webkom/lego-bricks'; +import { ConfirmModal, Flex, LoadingIndicator } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import { Field } from 'react-final-form'; import { useNavigate, useParams } from 'react-router-dom'; @@ -97,71 +97,73 @@ const LendableObjectEdit = () => { } : {}; - const showSkeleton = !(isNew || (lendableObject && groups)); + const showLoading = !(isNew || (lendableObject && groups)); return ( - - - {({ handleSubmit }) => ( -
- - - - - - - - - {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} - - {!isNew && ( - - {({ openConfirmModal }) => ( - - )} - - )} - - - )} -
+ + + + {({ handleSubmit }) => ( +
+ + + + + + + + + {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} + + {!isNew && ( + + {({ openConfirmModal }) => ( + + )} + + )} + + + )} +
+
); }; diff --git a/app/routes/lending/components/LendableObjectsList.css b/app/routes/lending/components/LendableObjectsList.css index 2a2d2cfdb8..d12430e883 100644 --- a/app/routes/lending/components/LendableObjectsList.css +++ b/app/routes/lending/components/LendableObjectsList.css @@ -29,7 +29,6 @@ align-items: stretch; padding: 0; height: 100%; - max-height: 20rem; } .lendableObjectImage { diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index 7a3444196d..4ca3a58e59 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -79,16 +79,9 @@ export const LendableObjectsList = () => { {lendingRequests.length === 0 ? (

Her var det tomt!

) : ( - lendingRequests - // TODO: does not work atm.. - // .sort((a, b) => b.endDate.diff(a.endDate)) - // .filter( - // (req) => - // showOldRequests || req.endDate.isAfter(moment().startOf('day')) - // ) - .map((request) => ( - - )) + lendingRequests.map((request) => ( + + )) )}
diff --git a/app/routes/lending/components/LendingAdmin.css b/app/routes/lending/components/LendingAdmin.css index 2be86fb263..9576e85f2c 100644 --- a/app/routes/lending/components/LendingAdmin.css +++ b/app/routes/lending/components/LendingAdmin.css @@ -1,8 +1,7 @@ @import url('~app/styles/variables.css'); .heading { - padding-bottom: var(--spacing-sm); - margin-bottom: var(--spacing-lg); + margin-top: var(--spacing-xl); } .newLendableObject { @@ -21,6 +20,11 @@ } } +.lendingRequestsContainer { + border-bottom: 1px solid var(--border-gray); + padding-bottom: var(--spacing-lg); +} + .lendableObjectsContainer { display: grid; grid-template-columns: repeat(3, 1fr); diff --git a/app/routes/lending/components/LendingAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx index b28dcea8f4..361b7d1a19 100644 --- a/app/routes/lending/components/LendingAdmin.tsx +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -1,10 +1,4 @@ -import { - Button, - Card, - Flex, - Icon, - LoadingIndicator, -} from '@webkom/lego-bricks'; +import { Button, Card, Flex, LoadingIndicator } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; @@ -58,49 +52,55 @@ const LendableObjectsAdmin = () => { path: '/lending', }} /> -

Ventende utlånsforespørsler

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

Tidligere utlånsforespørsler

- +
+

Ventende utlånsforespørsler

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

Utlånsobjekter

- - + - - - + + } + /> + + +
{lendableObjects.map((lendableObject) => ( { hideOverflow className={styles.lendableObjectCard} > -

- {lendableObject.id} - {lendableObject.title} -

+

{lendableObject.title}

))}
-
+
); }; diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 3e1c554246..e2b4a05ce1 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -76,7 +76,7 @@ const LendingRequest = () => {
-

Beskjed:

+

Kommentar:

{request.message}
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 index 9a22a7fdda..a985f02ab9 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -24,6 +24,8 @@ import { 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'; type Params = { lendingRequestId: string; @@ -63,8 +65,10 @@ const LendingRequestAdmin = () => { }), ); - const otherApprovedRequests = otherRequests.filter((loan) => !loan.pending); - const otherPendingRequests = otherRequests.filter((loan) => loan.pending); + console.log(otherRequests) + + const otherApprovedRequests = otherRequests.filter((loan) => loan.status === LendingRequestStatus.APPROVED); + const otherPendingRequests = otherRequests.filter((loan) => loan.status === LendingRequestStatus.PENDING); if (!request) { return

Ukjent forespørsel

; @@ -100,10 +104,10 @@ const LendingRequestAdmin = () => { const infoItems = [ { key: 'Status', - value: request.pending ? 'Venter på svar' : 'Godkjent', + value: statusToString(request.status), }, { - key: 'Tidsspenn', + key: 'Lånetid', value: , }, { @@ -136,7 +140,7 @@ const LendingRequestAdmin = () => {
-

Beskjed:

+

Kommentar:

{request.message}
@@ -145,7 +149,7 @@ const LendingRequestAdmin = () => {
- + Date: Sat, 4 May 2024 09:51:48 +0200 Subject: [PATCH 40/44] Remove unnecessary type 'Params' --- app/routes/lending/components/LendableObjectAdminDetail.tsx | 3 +-- app/routes/lending/components/LendableObjectDetail.tsx | 6 +----- app/routes/lending/components/LendableObjectEdit.tsx | 6 +----- app/routes/lending/components/LendingRequest.tsx | 6 +----- app/routes/lending/components/LendingRequestAdmin.tsx | 6 +----- 5 files changed, 5 insertions(+), 22 deletions(-) diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index 09c797c024..7df4298b5f 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -13,10 +13,9 @@ import NavigationTab from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -import type { Params } from 'react-router-dom'; const LendableObjectAdminDetail = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams(); const dispatch = useAppDispatch(); diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 1a151879c7..8e48bf7d29 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -20,12 +20,8 @@ import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -type Params = { - lendableObjectId: string; -}; - const LendableObjectDetail = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams(); const [showLendingForm, setShowLendingForm] = useState(false); const [start, setStart] = useState(''); const [end, setEnd] = useState(''); diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 4a533621c6..c8fee43d01 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -25,14 +25,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { roleOptions } from 'app/utils/constants'; import type { EditingLendableObject } from 'app/store/models/LendableObject'; -type Params = { - lendableObjectId: string | undefined; -}; - const TypedLegoForm = LegoFinalForm; const LendableObjectEdit = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams(); const isNew = lendableObjectId === undefined; const dispatch = useAppDispatch(); diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index e2b4a05ce1..3d530df327 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -14,12 +14,8 @@ import { FromToTime } from 'app/components/Time'; import { selectLendingRequestById } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -type LendingRequestParams = { - lendingRequestId: string; -}; - const LendingRequest = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams(); const dispatch = useAppDispatch(); usePreparedEffect( diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index a985f02ab9..ff97511212 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -27,12 +27,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { LendingRequestStatus, statusToString } from 'app/store/models/LendingRequest'; import styles from './LendingRequestAdmin.css'; -type Params = { - lendingRequestId: string; -}; - const LendingRequestAdmin = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams(); const dispatch = useAppDispatch(); usePreparedEffect( From ba663a90e2a8b701eaa5f1c98477e1e30f378a08 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Sun, 5 May 2024 09:10:09 +0200 Subject: [PATCH 41/44] Revert "Remove unnecessary type 'Params'" This reverts commit f1ff1f1380b38494a98cb15759b3ccb3e68dbcc6. --- app/routes/lending/components/LendableObjectAdminDetail.tsx | 3 ++- app/routes/lending/components/LendableObjectDetail.tsx | 6 +++++- app/routes/lending/components/LendableObjectEdit.tsx | 6 +++++- app/routes/lending/components/LendingRequest.tsx | 6 +++++- app/routes/lending/components/LendingRequestAdmin.tsx | 6 +++++- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index 7df4298b5f..09c797c024 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -13,9 +13,10 @@ import NavigationTab from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import type { Params } from 'react-router-dom'; const LendableObjectAdminDetail = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams(); const dispatch = useAppDispatch(); diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 8e48bf7d29..1a151879c7 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -20,8 +20,12 @@ import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +type Params = { + lendableObjectId: string; +}; + const LendableObjectDetail = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams(); const [showLendingForm, setShowLendingForm] = useState(false); const [start, setStart] = useState(''); const [end, setEnd] = useState(''); diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index c8fee43d01..4a533621c6 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -25,10 +25,14 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { roleOptions } from 'app/utils/constants'; import type { EditingLendableObject } from 'app/store/models/LendableObject'; +type Params = { + lendableObjectId: string | undefined; +}; + const TypedLegoForm = LegoFinalForm; const LendableObjectEdit = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams(); const isNew = lendableObjectId === undefined; const dispatch = useAppDispatch(); diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 3d530df327..e2b4a05ce1 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -14,8 +14,12 @@ import { FromToTime } from 'app/components/Time'; import { selectLendingRequestById } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +type LendingRequestParams = { + lendingRequestId: string; +}; + const LendingRequest = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams(); const dispatch = useAppDispatch(); usePreparedEffect( diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index ff97511212..a985f02ab9 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -27,8 +27,12 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { LendingRequestStatus, statusToString } from 'app/store/models/LendingRequest'; import styles from './LendingRequestAdmin.css'; +type Params = { + lendingRequestId: string; +}; + const LendingRequestAdmin = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams(); const dispatch = useAppDispatch(); usePreparedEffect( From 7d09e247354545a6b248d61d267a735d7141f344 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Sun, 5 May 2024 09:14:29 +0200 Subject: [PATCH 42/44] Properly type Params --- .../components/LendableObjectAdminDetail.tsx | 3 +-- .../components/LendableObjectDetail.tsx | 6 +----- .../lending/components/LendableObjectEdit.tsx | 6 +----- .../lending/components/LendingRequest.tsx | 6 +----- .../components/LendingRequestAdmin.tsx | 21 +++++++++++-------- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index 09c797c024..1fda6e9bb0 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -13,10 +13,9 @@ import NavigationTab from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -import type { Params } from 'react-router-dom'; const LendableObjectAdminDetail = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams<{ lendableObjectId: string }>(); const dispatch = useAppDispatch(); diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 1a151879c7..78863ddaa1 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -20,12 +20,8 @@ import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -type Params = { - lendableObjectId: string; -}; - const LendableObjectDetail = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams<{ lendableObjectId: string }>(); const [showLendingForm, setShowLendingForm] = useState(false); const [start, setStart] = useState(''); const [end, setEnd] = useState(''); diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 4a533621c6..4d49f217e3 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -25,14 +25,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { roleOptions } from 'app/utils/constants'; import type { EditingLendableObject } from 'app/store/models/LendableObject'; -type Params = { - lendableObjectId: string | undefined; -}; - const TypedLegoForm = LegoFinalForm; const LendableObjectEdit = () => { - const { lendableObjectId } = useParams(); + const { lendableObjectId } = useParams<{ lendableObjectId: string }>(); const isNew = lendableObjectId === undefined; const dispatch = useAppDispatch(); diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index e2b4a05ce1..af41f2485d 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -14,12 +14,8 @@ import { FromToTime } from 'app/components/Time'; import { selectLendingRequestById } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -type LendingRequestParams = { - lendingRequestId: string; -}; - const LendingRequest = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams<{ lendingRequestId: string }>(); const dispatch = useAppDispatch(); usePreparedEffect( diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index a985f02ab9..55b4c5d724 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -24,15 +24,14 @@ import { selectLendingRequestsByLendableObjectId, } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; -import { LendingRequestStatus, statusToString } from 'app/store/models/LendingRequest'; +import { + LendingRequestStatus, + statusToString, +} from 'app/store/models/LendingRequest'; import styles from './LendingRequestAdmin.css'; -type Params = { - lendingRequestId: string; -}; - const LendingRequestAdmin = () => { - const { lendingRequestId } = useParams(); + const { lendingRequestId } = useParams<{ lendingRequestId: string }>(); const dispatch = useAppDispatch(); usePreparedEffect( @@ -65,10 +64,14 @@ const LendingRequestAdmin = () => { }), ); - console.log(otherRequests) + console.log(otherRequests); - const otherApprovedRequests = otherRequests.filter((loan) => loan.status === LendingRequestStatus.APPROVED); - const otherPendingRequests = otherRequests.filter((loan) => loan.status === LendingRequestStatus.PENDING); + const otherApprovedRequests = otherRequests.filter( + (loan) => loan.status === LendingRequestStatus.APPROVED, + ); + const otherPendingRequests = otherRequests.filter( + (loan) => loan.status === LendingRequestStatus.PENDING, + ); if (!request) { return

Ukjent forespørsel

; From 2506bda4d56b56db3dcc387b44b7ddc36ff17fb1 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Thu, 9 May 2024 18:06:41 +0200 Subject: [PATCH 43/44] Fix comments --- app/actions/LendableObjectActions.ts | 4 ++-- app/routes/lending/components/LendingAdmin.tsx | 9 +++++---- app/routes/lending/components/LendingRequest.tsx | 2 +- app/routes/lending/components/LendingRequestAdmin.tsx | 10 ++++------ 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts index 88e7eb507c..d97321f47f 100644 --- a/app/actions/LendableObjectActions.ts +++ b/app/actions/LendableObjectActions.ts @@ -44,7 +44,7 @@ export function deleteLendableObject(id: EntityId) { } export function createLendableObject(data) { - return callAPI({ + return callAPI({ types: LendableObject.CREATE, endpoint: '/lendableobject/', method: 'POST', @@ -57,7 +57,7 @@ export function createLendableObject(data) { } export function editLendableObject({ id, ...data }) { - return callAPI({ + return callAPI({ types: LendableObject.EDIT, endpoint: `/lendableobject/${id}/`, method: 'PATCH', diff --git a/app/routes/lending/components/LendingAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx index 361b7d1a19..66d5ffa0b3 100644 --- a/app/routes/lending/components/LendingAdmin.tsx +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -21,10 +21,11 @@ const LendableObjectsAdmin = () => { usePreparedEffect( 'fetchAllLendingObjectsAndRequests', - () => { - dispatch(fetchAllLendingRequests()); - dispatch(fetchAllLendableObjects()); - }, + () => + Promise.allSettled([ + dispatch(fetchAllLendingRequests()), + dispatch(fetchAllLendableObjects()), + ]), [], ); diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index af41f2485d..58bfceb3c6 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -21,7 +21,7 @@ const LendingRequest = () => { usePreparedEffect( 'fetchRequest', () => lendingRequestId && dispatch(fetchLendingRequest(lendingRequestId)), - [], + [lendingRequestId], ); const request = useAppSelector((state) => diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index 55b4c5d724..45db2f2d9b 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -35,9 +35,9 @@ const LendingRequestAdmin = () => { const dispatch = useAppDispatch(); usePreparedEffect( - 'fetchRequest', - () => dispatch(fetchLendingRequest(lendingRequestId)), - [], + 'fetchLendingRequest', + () => lendingRequestId && dispatch(fetchLendingRequest(lendingRequestId)), + [lendingRequestId], ); const request = useAppSelector((state) => @@ -47,7 +47,7 @@ const LendingRequestAdmin = () => { const fetching = useAppSelector((state) => state.lendableObjects.fetching); usePreparedEffect( - 'fetchRequests', + 'fetchLendingRequests', () => { if (request && request.lendableObject?.id) { dispatch( @@ -64,8 +64,6 @@ const LendingRequestAdmin = () => { }), ); - console.log(otherRequests); - const otherApprovedRequests = otherRequests.filter( (loan) => loan.status === LendingRequestStatus.APPROVED, ); From 0af454b50f8baac17a56efab8228b8580be88766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eik=20Hvattum=20R=C3=B8geberg?= Date: Fri, 5 Jul 2024 22:26:16 +0200 Subject: [PATCH 44/44] Update for new page layout components --- app/reducers/index.ts | 3 + app/reducers/lendingRequests.ts | 2 +- .../components/LendableObjectAdminDetail.tsx | 31 ++++---- .../components/LendableObjectDetail.tsx | 30 ++++---- .../lending/components/LendableObjectEdit.tsx | 30 +++++--- .../components/LendableObjectsList.tsx | 24 ++++--- .../lending/components/LendingAdmin.css | 4 -- .../lending/components/LendingAdmin.tsx | 52 +++++++------- .../lending/components/LendingRequest.tsx | 57 +++++++++------ .../components/LendingRequestAdmin.tsx | 70 +++++++++---------- app/store/models/LendingRequest.ts | 12 ++-- 11 files changed, 170 insertions(+), 145 deletions(-) diff --git a/app/reducers/index.ts b/app/reducers/index.ts index dc0ff89529..9273fde290 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -162,4 +162,7 @@ export const lendableObjectSchema = new schema.Entity( ); export const lendingRequestSchema = new schema.Entity( EntityType.LendingRequests, + { + lendableObject: lendableObjectSchema, + }, ); diff --git a/app/reducers/lendingRequests.ts b/app/reducers/lendingRequests.ts index b91549de1b..8fc38288d0 100644 --- a/app/reducers/lendingRequests.ts +++ b/app/reducers/lendingRequests.ts @@ -25,4 +25,4 @@ export const { } = legoAdapter.getSelectors((state: RootState) => state.lendingRequests); export const selectLendingRequestsByLendableObjectId = - selectLendingRequestsByField('lendableObjectId'); + selectLendingRequestsByField('lendableObject'); diff --git a/app/routes/lending/components/LendableObjectAdminDetail.tsx b/app/routes/lending/components/LendableObjectAdminDetail.tsx index 1fda6e9bb0..11d60ca881 100644 --- a/app/routes/lending/components/LendableObjectAdminDetail.tsx +++ b/app/routes/lending/components/LendableObjectAdminDetail.tsx @@ -2,14 +2,12 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import { LoadingIndicator } from '@webkom/lego-bricks'; +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 { Content } from 'app/components/Content'; -import NavigationTab from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -42,20 +40,21 @@ const LendableObjectAdminDetail = () => { ); if (!lendableObject) { - return ( - - - - ); + return ; } - const title = `Godkjenn utlån av ${lendableObject.title}`; + const title = `Admin: Godkjenn utlån av ${lendableObject.title}`; return ( - - - - - + + + { }} events={[lendingRequests]} /> - - + + ); }; diff --git a/app/routes/lending/components/LendableObjectDetail.tsx b/app/routes/lending/components/LendableObjectDetail.tsx index 78863ddaa1..baa37f0d18 100644 --- a/app/routes/lending/components/LendableObjectDetail.tsx +++ b/app/routes/lending/components/LendableObjectDetail.tsx @@ -2,7 +2,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import { Modal } from '@webkom/lego-bricks'; +import { LinkButton, Modal, Page, PageCover } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import moment from 'moment-timezone'; import { useState } from 'react'; @@ -11,12 +11,10 @@ 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 { Content } from 'app/components/Content'; 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 NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -66,16 +64,22 @@ const LendableObjectDetail = () => { const title = `Utlån av ${lendableObject?.title}`; return ( - - - - - + } + actionButtons={ + Rediger - - + + } + skeleton={fetchingObjects} + > + - + {lendableObject && 'description' in lendableObject && ( + + )} { }} /> - setShowLendingForm(false)}> + { }} - + ); }; diff --git a/app/routes/lending/components/LendableObjectEdit.tsx b/app/routes/lending/components/LendableObjectEdit.tsx index 4d49f217e3..272d05ecd8 100644 --- a/app/routes/lending/components/LendableObjectEdit.tsx +++ b/app/routes/lending/components/LendableObjectEdit.tsx @@ -1,4 +1,9 @@ -import { ConfirmModal, Flex, LoadingIndicator } from '@webkom/lego-bricks'; +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'; @@ -8,7 +13,6 @@ import { editLendableObject, fetchLendableObject, } from 'app/actions/LendableObjectActions'; -import { Content } from 'app/components/Content'; import { Button, EditorField, @@ -23,7 +27,10 @@ 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 { EditingLendableObject } from 'app/store/models/LendableObject'; +import type { + DetailedLendableObject, + EditingLendableObject, +} from 'app/store/models/LendableObject'; const TypedLegoForm = LegoFinalForm; @@ -41,7 +48,7 @@ const LendableObjectEdit = () => { ); const lendableObject = useAppSelector((state) => - selectLendableObjectById(state, lendableObjectId), + selectLendableObjectById(state, lendableObjectId), ); const groups = useAppSelector(selectAllGroups); @@ -94,8 +101,11 @@ const LendableObjectEdit = () => { : {}; const showLoading = !(isNew || (lendableObject && groups)); + const title = isNew + ? 'Opprett utlånsobjekt' + : `Redigerer ${lendableObject?.title}`; return ( - + { component={TextInput.Field} /> - + {isNew ? 'Opprett utlånsobjekt' : 'Lagre endringer'} {!isNew && ( {({ openConfirmModal }) => ( - )} )} - + )} - + ); }; diff --git a/app/routes/lending/components/LendableObjectsList.tsx b/app/routes/lending/components/LendableObjectsList.tsx index 4ca3a58e59..a20213dc02 100644 --- a/app/routes/lending/components/LendableObjectsList.tsx +++ b/app/routes/lending/components/LendableObjectsList.tsx @@ -1,4 +1,11 @@ -import { Button, Card, LoadingIndicator } from '@webkom/lego-bricks'; +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'; @@ -6,10 +13,7 @@ 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 { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; -import { Image } from 'app/components/Image'; -import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { selectAllLendableObjects } from 'app/reducers/lendableObjects'; import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -67,11 +71,11 @@ export const LendableObjectsList = () => { const title = 'Utlån'; return ( - + Admin} + > - - Admin -

Mine forespørsler

@@ -87,7 +91,7 @@ export const LendableObjectsList = () => {
{lendingRequests.length !== 0 && ( -
-
+ ); }; diff --git a/app/routes/lending/components/LendingAdmin.css b/app/routes/lending/components/LendingAdmin.css index 9576e85f2c..27531d6973 100644 --- a/app/routes/lending/components/LendingAdmin.css +++ b/app/routes/lending/components/LendingAdmin.css @@ -1,9 +1,5 @@ @import url('~app/styles/variables.css'); -.heading { - margin-top: var(--spacing-xl); -} - .newLendableObject { display: flex; align-items: center; diff --git a/app/routes/lending/components/LendingAdmin.tsx b/app/routes/lending/components/LendingAdmin.tsx index 66d5ffa0b3..a7d57d08ac 100644 --- a/app/routes/lending/components/LendingAdmin.tsx +++ b/app/routes/lending/components/LendingAdmin.tsx @@ -1,12 +1,17 @@ -import { Button, Card, Flex, LoadingIndicator } from '@webkom/lego-bricks'; +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 { Content } from 'app/components/Content'; -import NavigationTab from 'app/components/NavigationTab'; import { selectAllLendableObjects } from 'app/reducers/lendableObjects'; import { selectAllLendingRequests } from 'app/reducers/lendingRequests'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -42,19 +47,22 @@ const LendableObjectsAdmin = () => { (state) => state.lendingRequests.fetching, ); - const title = 'Utlånsforepørsler'; + const title = 'Admin: Utlånssystem'; return ( - + Nytt utlånsobjekt + } + > - + +

Utlånsforepørsler

-

Ventende utlånsforespørsler

+

Ventende utlånsforespørsler

{lendingRequests @@ -69,7 +77,7 @@ const LendableObjectsAdmin = () => { {showOldRequests ? ( <> -

Tidligere utlånsforespørsler

+

Tidligere utlånsforespørsler

{lendingRequests .filter( @@ -79,26 +87,18 @@ const LendableObjectsAdmin = () => { ))} - ) : ( - )}
- - - - } - /> +

Utlånsobjekter

@@ -120,7 +120,7 @@ const LendableObjectsAdmin = () => { -
+ ); }; diff --git a/app/routes/lending/components/LendingRequest.tsx b/app/routes/lending/components/LendingRequest.tsx index 58bfceb3c6..0efbac03d9 100644 --- a/app/routes/lending/components/LendingRequest.tsx +++ b/app/routes/lending/components/LendingRequest.tsx @@ -1,18 +1,19 @@ +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 { - Content, ContentMain, ContentSection, ContentSidebar, } from 'app/components/Content'; import InfoList from 'app/components/InfoList'; -import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; 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 }>(); @@ -24,56 +25,66 @@ const LendingRequest = () => { [lendingRequestId], ); - const request = useAppSelector((state) => + 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: request?.pending ? 'Venter på svar' : 'Godkjent', + value: statusToString(lendingRequest.status), }, { key: 'Tidsspenn', - value: , + value: ( + + ), }, { key: 'Bruker', value: ( - - {request?.author?.fullName} + + {lendingRequest.author.fullName} ), }, ]; - const title = `Forespørsel om utlån av ${request?.lendableObject.title}`; + const title = `Forespørsel om utlån av ${lendableObject.title}`; return ( - - {request && ( + + Admin + + } + skeleton={requestFetching} + > + {lendingRequest && ( <> - - - Admin - -

Kommentar:

- {request.message} + {lendingRequest.message}
@@ -82,7 +93,7 @@ const LendingRequest = () => {
)} -
+ ); }; diff --git a/app/routes/lending/components/LendingRequestAdmin.tsx b/app/routes/lending/components/LendingRequestAdmin.tsx index 45db2f2d9b..ad47b7d57c 100644 --- a/app/routes/lending/components/LendingRequestAdmin.tsx +++ b/app/routes/lending/components/LendingRequestAdmin.tsx @@ -2,8 +2,9 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; -import { LoadingIndicator } from '@webkom/lego-bricks'; +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 { @@ -11,14 +12,13 @@ import { fetchLendingRequestsForLendableObject, } from 'app/actions/LendingRequestActions'; import { - Content, ContentMain, ContentSection, ContentSidebar, } from 'app/components/Content'; import InfoList from 'app/components/InfoList'; -import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import { FromToTime } from 'app/components/Time'; +import { selectLendableObjectById } from 'app/reducers/lendableObjects'; import { selectLendingRequestById, selectLendingRequestsByLendableObjectId, @@ -40,28 +40,32 @@ const LendingRequestAdmin = () => { [lendingRequestId], ); - const request = useAppSelector((state) => + 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 (request && request.lendableObject?.id) { + if (lendingRequest && lendingRequest.lendableObject) { dispatch( - fetchLendingRequestsForLendableObject(request.lendableObject.id), + fetchLendingRequestsForLendableObject(lendingRequest.lendableObject), ); } }, - [request?.lendableObject.id], + [lendingRequest?.lendableObject], ); const otherRequests = useAppSelector((state) => - selectLendingRequestsByLendableObjectId(state, { - lendableObjectId: request?.lendableObject.id, - }), + selectLendingRequestsByLendableObjectId( + state, + lendingRequest?.lendableObject, + ), ); const otherApprovedRequests = otherRequests.filter( @@ -71,22 +75,22 @@ const LendingRequestAdmin = () => { (loan) => loan.status === LendingRequestStatus.PENDING, ); - if (!request) { + if (!lendingRequest) { return

Ukjent forespørsel

; } const requestEvent = { - id: String(request.id), - title: request.author?.fullName, - start: request.startDate, - end: request.endDate, + 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: request.author?.fullName, + title: lendingRequest.author?.fullName, start: loan.startDate, end: loan.endDate, backgroundColor: 'var(--color-gray-5)', @@ -95,7 +99,7 @@ const LendingRequestAdmin = () => { const otherPendingEvents = otherPendingRequests.map((loan) => ({ id: String(loan.id), - title: request?.author?.fullName, + title: lendingRequest?.author?.fullName, start: loan.startDate, end: loan.endDate, backgroundColor: 'var(--color-red-2)', @@ -105,44 +109,38 @@ const LendingRequestAdmin = () => { const infoItems = [ { key: 'Status', - value: statusToString(request.status), + value: statusToString(lendingRequest.status), }, { key: 'Lånetid', - value: , + value: ( + + ), }, { key: 'Bruker', value: ( - - {request.author?.fullName} + + {lendingRequest.author?.fullName} ), }, ]; - const title = `Forespørsel om utlån av ${request.lendableObject.title}`; + const title = `Admin: Forespørsel om utlån av ${lendableObject?.title}`; return ( - + - - - Admin - -

Kommentar:

- {request.message} + {lendingRequest.message}
@@ -175,7 +173,7 @@ const LendingRequestAdmin = () => {
-
+ ); }; diff --git a/app/store/models/LendingRequest.ts b/app/store/models/LendingRequest.ts index 58230ce61c..6ae0cc5bbf 100644 --- a/app/store/models/LendingRequest.ts +++ b/app/store/models/LendingRequest.ts @@ -1,6 +1,6 @@ -import type { ListLendableObject } from './LendableObject'; +import type { EntityId } from '@reduxjs/toolkit'; +import type { Dateish } from 'app/models'; import type { PublicUser } from 'app/store/models/User'; -import type moment from 'moment-timezone'; export enum LendingRequestStatus { PENDING = 'PENDING', @@ -11,7 +11,7 @@ export enum LendingRequestStatus { export function statusToString(status: LendingRequestStatus): string { switch (status) { case LendingRequestStatus.PENDING: - return 'Venter'; + return 'Venter på svar'; case LendingRequestStatus.APPROVED: return 'Godkjent'; case LendingRequestStatus.DENIED: @@ -22,9 +22,9 @@ export function statusToString(status: LendingRequestStatus): string { export type LendingRequest = { id: number; author: PublicUser; - startDate: moment.Moment; - endDate: moment.Moment; + startDate: Dateish; + endDate: Dateish; message: string; status: LendingRequestStatus; - lendableObject: ListLendableObject; + lendableObject: EntityId; };