From 61393b1eccd47ba9920d9c1380ebeecdb61b4ef7 Mon Sep 17 00:00:00 2001 From: Marc Buma Date: Wed, 6 Mar 2024 01:51:51 +0100 Subject: [PATCH 01/15] various edits - fix wrong status code in delete CRUD action - add aanmaken button to app header - add selection for proposed parkings to filterbox / filterslice - hide stalling aanmelden when logged in - update fietsenstalling filters for proposed parkings - revise mechanism for creating / proposing parkings - remove unused files - some extra types / typescript fixes --- scripts/create-test-user.js | 108 +++++++++++++++++++++ src/backend/handlers/crud-route-handler.ts | 2 +- src/components/AppHeaderDesktop.tsx | 76 +++++++++------ src/components/Button.tsx | 15 ++- src/components/FooterNav.tsx | 32 +++--- 5 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 scripts/create-test-user.js diff --git a/scripts/create-test-user.js b/scripts/create-test-user.js new file mode 100644 index 0000000..f268d2c --- /dev/null +++ b/scripts/create-test-user.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const bcrypt = require('bcrypt'); + +const c_root_admin = 1; // Super Admin +const c_intern_admin = 2; // Admin (intern) +const c_intern_editor = 3; // Redacteur (intern) +const c_extern_admin = 4; // Admin (gemeente) +const c_extern_editor = 5; // Redacteur (gemeente) +const c_exploitant = 6; // Exploitant +const c_beheerder = 7; // Beheerder +// const c_exploitant_data_analyst: number = 8; // Exploitant data analyst -> disabled +const c_intern_data_analyst = 9; +// const c_extern_data_analyst: number = 10; // Extern data analyst -> disabled + +const saltRounds = 13; // 13 salt rounds used in the original code + +const stallingenIDs_utrecht = [ + 'E197BE1D-B9CC-9B59-D88D7A356D6FEEE8', + 'E197C7FD-C31E-050A-EAB81894CEB8C946', + 'E197C889-9EB9-5F9B-AEA4F4D7B993E795', + 'E197CD25-9F4F-A2D5-26007A22650A4DC0', + 'E197D9DC-C0B9-D9B2-9737C96AED1E68B0', + 'E197DD9B-C7B6-C306-07C6F96E7F42A79B', + 'E197EB66-A859-3EDA-D243C3789EEAE4F0', + 'E198D753-B00C-0F41-9A8C0F275D822E6D', + 'E199038B-DF30-10EB-93C7A52E7AA26808', + 'E1991A95-08EF-F11D-FF946CE1AA0578FB', + 'E199235E-A49E-734E-67A5836FA2358C14', + 'E1994219-9047-F340-067A1D64587BC21D', + 'E1994396-D16A-35F2-2D710CBCEC414338', +] + +// +++++++++++++++++++++++++++++++++++++++++++ +// Fill these fields + +const username = ""; +const email = ""; +const password = ""; +const roleID = c_root_admin; + +// +++++++++++++++++++++++++++++++++++++++++++ +function generateCustomId() { + // Function to generate a random hex string of a given length + const randomHex = (length) => { + let result = ''; + const characters = '0123456789ABCDEF'; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; + } + + // Generate segments of the custom ID + const part1 = randomHex(8); + const part2 = randomHex(4); + const part3 = randomHex(4); + const part4 = randomHex(16); + + // Combine segments into the custom ID format + return `${part1}-${part2}-${part3}-${part4}`; +} + +const hashedPassword = bcrypt.hashSync(password, saltRounds); +// const newUserUUID = generateCustomId(); +const newUserUUID = "D4351342-685D-D17A-B3617EEBBF39451C"; + + +const sqluser = `INSERT INTO 'security_users' ( + UserID, + Locale, + RoleID, + GroupID, + SiteID, + ParentID, + UserName, + EncryptedPassword, + EncryptedPassword2, + DisplayName, + LastLogin, + SendMailToMailAddress, + Theme, + Status) VALUES ( + '${newUserUUID}', + 'Dutch (Standard)', + ${roleID}, + 'intern', + NULL, + '', + '${email}', + '${hashedPassword}', + '', + '${username}', + '2021-04-06 22:58:13', + NULL, + 'default', + '1' + );`; + + const sql = []; + sql.push(sqluser); + +stallingenIDs_utrecht.forEach((stallingID, idx) => { + const sqlstalling = `INSERT INTO security_users_sites (ID,UserID,SiteID,IsContact) VALUES (${1000000+idx},'${newUserUUID}','${stallingID}','0');` + sql.push(sqlstalling); +}); + +fs.writeFileSync('create-test-user.sql', sql.join('\n')); +// console.log(sql); diff --git a/src/backend/handlers/crud-route-handler.ts b/src/backend/handlers/crud-route-handler.ts index 874754e..48fb71c 100644 --- a/src/backend/handlers/crud-route-handler.ts +++ b/src/backend/handlers/crud-route-handler.ts @@ -47,7 +47,7 @@ export const CrudRouteHandler = async ( if ((request.method as HttpMethod) === "DELETE") { const deleteResponse = await service.delete(id); - return response.status(20).json(deleteResponse); + return response.status(200).json(deleteResponse); } return response.status(405).send("Method not allowed"); diff --git a/src/components/AppHeaderDesktop.tsx b/src/components/AppHeaderDesktop.tsx index a1b3f2f..df33437 100644 --- a/src/components/AppHeaderDesktop.tsx +++ b/src/components/AppHeaderDesktop.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useRouter } from "next/navigation"; import { useSession, signOut } from "next-auth/react" @@ -18,7 +18,6 @@ import { filterNavItemsBasedOnMapZoom, getPrimary, getSecundary, - getFooter } from "~/utils/navigation"; const PrimaryMenuItem = (props: any) => { @@ -33,7 +32,7 @@ const PrimaryMenuItem = (props: any) => { push(props.url); }}> - {props.icon ? : ''} + {props.icon ? : ''} {props.title} @@ -65,14 +64,14 @@ function AppHeaderDesktop({ const { push } = useRouter(); const pathName = usePathname(); const { data: session } = useSession() - + const [articles, setArticles] = useState([]); const [fietsberaadArticles, setFietsberaadArticles] = useState([]); const [didNavOverflow, setDidNavOverflow] = useState(false); - const isAuthenticated = useSelector( - (state: AppState) => state.auth.authState - ); + // const isAuthenticated = useSelector( + // (state: AppState) => state.auth.authState + // ); const activeMunicipalityInfo = useSelector( (state: AppState) => state.map.activeMunicipalityInfo @@ -84,7 +83,7 @@ function AppHeaderDesktop({ useEffect(() => { // Get menu items from SiteID 1 OR SiteID of the municipality let SiteIdToGetArticlesFrom; - if(mapZoom >= 12 && activeMunicipalityInfo && activeMunicipalityInfo.ID) { + if (mapZoom >= 12 && activeMunicipalityInfo && activeMunicipalityInfo.ID) { SiteIdToGetArticlesFrom = activeMunicipalityInfo.ID; } else { SiteIdToGetArticlesFrom = "1"; @@ -94,7 +93,7 @@ function AppHeaderDesktop({ const response = await getNavigationItemsForMunicipality(SiteIdToGetArticlesFrom); setArticles(response); })(); - }, [ + }, [ activeMunicipalityInfo, pathName ]); @@ -125,12 +124,12 @@ function AppHeaderDesktop({ // Check if nav items overflow the nav bar let navOverflow = false; for (const el of wrapperEl.children) { - if(! el.classList.contains('PrimaryMenuItem')) { + if (!el.classList.contains('PrimaryMenuItem')) { continue; } const elementTop = el.offsetTop; const headerHeight = headerEl.offsetHeight; - if((elementTop + 12) >= headerHeight) {// 12 = padding-top of header + if ((elementTop + 12) >= headerHeight) {// 12 = padding-top of header el.style.display = 'none'; navOverflow = true; } else { @@ -140,8 +139,13 @@ function AppHeaderDesktop({ setDidNavOverflow(navOverflow); }; + + const handleNieuweStallingClick = () => { + push(`/?stallingid=aanmelden`); + } + const handleLoginClick = () => { - if(!session) { + if (!session) { push('/login'); } else { // sign out @@ -163,6 +167,8 @@ function AppHeaderDesktop({ const primaryMenuItems = getPrimary(allMenuItems) const secundaryMenuItems = getSecundary(allMenuItems); + const showStallingAanmaken = session && mapZoom >= 13 && activeMunicipalityInfo; + return ( <>
} - {primaryMenuItems ? primaryMenuItems.map((x,idx) => = 12 && activeMunicipalityInfo) ? activeMunicipalityInfo.UrlName : 'fietsberaad'}/${x.Title ? x.Title : ''}`} />) : ''}
+ display: didNavOverflow ? 'block' : 'none', + visibility: didNavOverflow ? 'visible' : 'hidden', + }}> { - dispatch(setIsMobileNavigationVisible(true)) - }} + style={{ height: '40px' }} + onClick={() => { + dispatch(setIsMobileNavigationVisible(true)) + }} />
+ {showStallingAanmaken ? + : null + }
- {secundaryMenuItems.map((x,idx) => { + {secundaryMenuItems.map((x, idx) => { return = 12 && activeMunicipalityInfo) ? activeMunicipalityInfo.UrlName : 'fietsberaad'}/${x.Title ? x.Title : ''}`} /> @@ -253,9 +278,6 @@ function AppHeaderDesktop({
- - {children} - ); } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 91ed193..30196a5 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -33,7 +33,7 @@ export const Button = ({ ${Styles[variant]} `} onClick={(e: MouseEvent) => { - if(onClick) onClick(e) + if (onClick) onClick(e) }} style={Object.assign({}, { userSelect: "none", // disable highlighting @@ -72,15 +72,14 @@ export const RadioButton = ({ transition-all bg-white ${className && className.indexOf('py-') > -1 ? '' : 'py-1'} - ${ - isActive - ? "border border-gray-700 shadow shadow-md" - : "border border-gray-200 bg-white text-gray-700 shadow shadow-sm" + ${isActive + ? "border border-gray-700 shadow shadow-md" + : "border border-gray-200 bg-white text-gray-700 shadow shadow-sm" } ${className} `} onClick={(e) => { - if(onClick) onClick(e) + if (onClick) onClick(e) }} style={{ userSelect: "none" }} // disable highlighting > @@ -99,7 +98,7 @@ export const IconButton = ({ style }: { onClick?: Function, - children: any + children?: any htmlBefore?: any, className?: string iconUrl?: string @@ -124,7 +123,7 @@ export const IconButton = ({ ${className} `} onClick={(e) => { - if(onClick) onClick(e) + if (onClick) onClick(e) }} style={Object.assign({}, { userSelect: "none", // disable highlighting diff --git a/src/components/FooterNav.tsx b/src/components/FooterNav.tsx index 02452f3..2028cc6 100644 --- a/src/components/FooterNav.tsx +++ b/src/components/FooterNav.tsx @@ -1,6 +1,7 @@ // @ts-nocheck -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react" import { getNavigationItemsForMunicipality, @@ -22,26 +23,28 @@ const FooterNavItem = ({ ${className} mx-2 `} - onClick={(e) => { - e.preventDefault(); + onClick={(e) => { + e.preventDefault(); push(url); - }} + }} > {children} } const FooterNav = () => { + const { data: session } = useSession() + const [fietsberaadArticles, setFietsberaadArticles] = useState([]); - // Get menu items for siteId 1 (Fietsberaad) + // Get menu items for siteId 1 (Fietsberaad) useEffect(() => { (async () => { const response = await getNavigationItemsForMunicipality(1); setFietsberaadArticles(response); })(); - }, []); + }, []); const navItemsPrimary = [ // { title: 'Stalling toevoegen' }, @@ -63,22 +66,23 @@ const FooterNav = () => { text-xs z-10 "> - - Stalling Aanmelden - + {!session ? + + Stalling Aanmelden + : null} {navItemsPrimary.map(x => - {x.title} + {x.title} )} - {footerMenuItems ? footerMenuItems.map((x,idx) => {x.DisplayTitle ? x.DisplayTitle : (x.Title ? x.Title : '')} From 113dbf20d89958b988327e8e121b764b62505b36 Mon Sep 17 00:00:00 2001 From: Marc Buma Date: Wed, 6 Mar 2024 01:52:49 +0100 Subject: [PATCH 02/15] - fixes related to creating new parkings / suggesting parkings - fixes related to showing suggested parkings - some typescript fixes / extra types - TODO: capacity / opening hours do not save correctly - TODO: rename suggested parking ID when saving from a logged in account - TODO: sync logic for opening hours from parkingedit to parkingview --- public/sw.js | 2 +- src/components/FilterBox.tsx | 37 +- src/components/Form/FormCheckbox.tsx | 6 +- src/components/MapComponent.tsx | 5 +- src/components/Parking.tsx | 70 ++-- src/components/ParkingFacilities.tsx | 24 +- src/components/ParkingFacilityBrowser.tsx | 47 ++- src/components/parking/ParkingEdit.tsx | 363 ++++++++++-------- .../parking/ParkingEditAfbeelding.tsx | 82 ++-- .../parking/ParkingEditCapaciteit.tsx | 149 +++---- src/components/parking/ParkingView.tsx | 6 +- .../parking/ParkingViewCapaciteit.tsx | 29 +- src/pages/api/auth/[...nextauth].ts | 17 +- src/pages/api/fietsenstallingen.ts | 13 + src/pages/api/parking/index.ts | 46 +-- src/pages/index.tsx | 68 ++-- src/pages/stalling-crud.tsx | 252 ------------ src/pages/stalling.tsx | 65 ---- src/store/filterSlice.ts | 51 ++- src/types/index.ts | 58 +-- src/utils/auth-tools.ts | 26 +- src/utils/parkings.tsx | 157 +++++++- src/utils/prisma.ts | 33 +- tsconfig.json | 17 +- 24 files changed, 823 insertions(+), 800 deletions(-) delete mode 100644 src/pages/stalling-crud.tsx delete mode 100644 src/pages/stalling.tsx diff --git a/public/sw.js b/public/sw.js index 5c07aec..56b8bb3 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const i=(i,a)=>(i=new URL(i+".js",a).href,s[i]||new Promise((s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()})).then((()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e})));self.define=(a,n)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let c={};const r=e=>i(e,o),t={module:{uri:o},exports:c,require:r};s[o]=Promise.all(a.map((e=>t[e]||r(e)))).then((e=>(n(...e),c)))}}define(["./workbox-7c2a5a06"],(function(e){"use strict";importScripts("fallback-MXmK67eIRgmHH81JehWbj.js"),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/MXmK67eIRgmHH81JehWbj/_buildManifest.js",revision:"3cb29316328f35a929b174b57d998d19"},{url:"/_next/static/MXmK67eIRgmHH81JehWbj/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/215-ac45c62bc2f2aa0a.js",revision:"ac45c62bc2f2aa0a"},{url:"/_next/static/chunks/292-841d98abcd9ff4be.js",revision:"841d98abcd9ff4be"},{url:"/_next/static/chunks/344-bcf92b16e0822376.js",revision:"bcf92b16e0822376"},{url:"/_next/static/chunks/506-f27b3660c9e376d4.js",revision:"f27b3660c9e376d4"},{url:"/_next/static/chunks/71-5a67a2e9b71e0fbf.js",revision:"5a67a2e9b71e0fbf"},{url:"/_next/static/chunks/717-a876f1ddc5f9c556.js",revision:"a876f1ddc5f9c556"},{url:"/_next/static/chunks/742-2ecc794b6dd82d8a.js",revision:"2ecc794b6dd82d8a"},{url:"/_next/static/chunks/780-57edcdee954dbbf5.js",revision:"57edcdee954dbbf5"},{url:"/_next/static/chunks/785-163e46434781d32a.js",revision:"163e46434781d32a"},{url:"/_next/static/chunks/999-767df9b77f3eaad3.js",revision:"767df9b77f3eaad3"},{url:"/_next/static/chunks/d7eeaac4-a1e8e8e53989db51.js",revision:"a1e8e8e53989db51"},{url:"/_next/static/chunks/d94c0b71-2242b2fc86a4947a.js",revision:"2242b2fc86a4947a"},{url:"/_next/static/chunks/dd81a582-41786d5833b74cce.js",revision:"41786d5833b74cce"},{url:"/_next/static/chunks/framework-63157d71ad419e09.js",revision:"63157d71ad419e09"},{url:"/_next/static/chunks/main-99f5e3f92a717c61.js",revision:"99f5e3f92a717c61"},{url:"/_next/static/chunks/pages/%5BurlName%5D-764c9af1ec6004e8.js",revision:"764c9af1ec6004e8"},{url:"/_next/static/chunks/pages/%5BurlName%5D/%5BpageName%5D-fd0c7822e4f3c1af.js",revision:"fd0c7822e4f3c1af"},{url:"/_next/static/chunks/pages/_app-c7ae4465be87fcfb.js",revision:"c7ae4465be87fcfb"},{url:"/_next/static/chunks/pages/_error-54de1933a164a1ff.js",revision:"54de1933a164a1ff"},{url:"/_next/static/chunks/pages/_offline-c64183d4136009ce.js",revision:"c64183d4136009ce"},{url:"/_next/static/chunks/pages/content-3c8b4c5e89f528e2.js",revision:"3c8b4c5e89f528e2"},{url:"/_next/static/chunks/pages/filter-9c14b224bd0b2bd0.js",revision:"9c14b224bd0b2bd0"},{url:"/_next/static/chunks/pages/index-274d016be14ef1b5.js",revision:"274d016be14ef1b5"},{url:"/_next/static/chunks/pages/loading-0cbccf288848fdff.js",revision:"0cbccf288848fdff"},{url:"/_next/static/chunks/pages/login-e02cd7cfa6a70c0d.js",revision:"e02cd7cfa6a70c0d"},{url:"/_next/static/chunks/pages/login/error-b3931065be16e833.js",revision:"b3931065be16e833"},{url:"/_next/static/chunks/pages/parking-facilities-600206f2e1663e5b.js",revision:"600206f2e1663e5b"},{url:"/_next/static/chunks/pages/stalling-017711df207f5040.js",revision:"017711df207f5040"},{url:"/_next/static/chunks/pages/stalling-crud-e56732d4cf22d13b.js",revision:"e56732d4cf22d13b"},{url:"/_next/static/chunks/pages/stalling/%5Bid%5D-e673659d39eca499.js",revision:"e673659d39eca499"},{url:"/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js",revision:"79330112775102f91e1010318bae2bd3"},{url:"/_next/static/chunks/webpack-f1c64600e64f11be.js",revision:"f1c64600e64f11be"},{url:"/_next/static/css/00841263f4ca408b.css",revision:"00841263f4ca408b"},{url:"/_next/static/css/2ba05e4ff1f637dc.css",revision:"2ba05e4ff1f637dc"},{url:"/_next/static/css/448f8af27fbceac0.css",revision:"448f8af27fbceac0"},{url:"/_next/static/css/4b424eaa5ffd81a5.css",revision:"4b424eaa5ffd81a5"},{url:"/_next/static/css/5f83e2849d67ba57.css",revision:"5f83e2849d67ba57"},{url:"/_next/static/css/963c15b87a85cc54.css",revision:"963c15b87a85cc54"},{url:"/_next/static/css/be83c9cb059e752e.css",revision:"be83c9cb059e752e"},{url:"/_next/static/css/da7f0d6de06a7fc1.css",revision:"da7f0d6de06a7fc1"},{url:"/_next/static/css/dc12f666bb62a0e1.css",revision:"dc12f666bb62a0e1"},{url:"/_next/static/css/ee5279d39fad2a37.css",revision:"ee5279d39fad2a37"},{url:"/_next/static/media/roboto-all-300-normal.39add8fb.woff",revision:"39add8fb"},{url:"/_next/static/media/roboto-all-400-normal.2e9e9400.woff",revision:"2e9e9400"},{url:"/_next/static/media/roboto-all-500-normal.d96daa81.woff",revision:"d96daa81"},{url:"/_next/static/media/roboto-all-700-normal.ca3d0fdb.woff",revision:"ca3d0fdb"},{url:"/_next/static/media/roboto-cyrillic-300-normal.88798412.woff2",revision:"88798412"},{url:"/_next/static/media/roboto-cyrillic-400-normal.2d9c9d60.woff2",revision:"2d9c9d60"},{url:"/_next/static/media/roboto-cyrillic-500-normal.aa68ea54.woff2",revision:"aa68ea54"},{url:"/_next/static/media/roboto-cyrillic-700-normal.258a358e.woff2",revision:"258a358e"},{url:"/_next/static/media/roboto-cyrillic-ext-300-normal.cd7c5715.woff2",revision:"cd7c5715"},{url:"/_next/static/media/roboto-cyrillic-ext-400-normal.d7827ae3.woff2",revision:"d7827ae3"},{url:"/_next/static/media/roboto-cyrillic-ext-500-normal.a1b5c90d.woff2",revision:"a1b5c90d"},{url:"/_next/static/media/roboto-cyrillic-ext-700-normal.dd3651fb.woff2",revision:"dd3651fb"},{url:"/_next/static/media/roboto-greek-300-normal.25dc89b0.woff2",revision:"25dc89b0"},{url:"/_next/static/media/roboto-greek-400-normal.63e6dc18.woff2",revision:"63e6dc18"},{url:"/_next/static/media/roboto-greek-500-normal.533b03d2.woff2",revision:"533b03d2"},{url:"/_next/static/media/roboto-greek-700-normal.432b858b.woff2",revision:"432b858b"},{url:"/_next/static/media/roboto-greek-ext-300-normal.bc5ce703.woff2",revision:"bc5ce703"},{url:"/_next/static/media/roboto-greek-ext-400-normal.2b547ded.woff2",revision:"2b547ded"},{url:"/_next/static/media/roboto-greek-ext-500-normal.7ea6cffa.woff2",revision:"7ea6cffa"},{url:"/_next/static/media/roboto-greek-ext-700-normal.a8d16efd.woff2",revision:"a8d16efd"},{url:"/_next/static/media/roboto-latin-300-normal.a4eae32d.woff2",revision:"a4eae32d"},{url:"/_next/static/media/roboto-latin-400-normal.f2894edc.woff2",revision:"f2894edc"},{url:"/_next/static/media/roboto-latin-500-normal.3170fd9a.woff2",revision:"3170fd9a"},{url:"/_next/static/media/roboto-latin-700-normal.71b2beb8.woff2",revision:"71b2beb8"},{url:"/_next/static/media/roboto-latin-ext-300-normal.37d4965d.woff2",revision:"37d4965d"},{url:"/_next/static/media/roboto-latin-ext-400-normal.21abc8c8.woff2",revision:"21abc8c8"},{url:"/_next/static/media/roboto-latin-ext-500-normal.85ebfb55.woff2",revision:"85ebfb55"},{url:"/_next/static/media/roboto-latin-ext-700-normal.6af98c24.woff2",revision:"6af98c24"},{url:"/_next/static/media/roboto-vietnamese-300-normal.b3d3e960.woff2",revision:"b3d3e960"},{url:"/_next/static/media/roboto-vietnamese-400-normal.c95fc061.woff2",revision:"c95fc061"},{url:"/_next/static/media/roboto-vietnamese-500-normal.7f8c0554.woff2",revision:"7f8c0554"},{url:"/_next/static/media/roboto-vietnamese-700-normal.72bf832f.woff2",revision:"72bf832f"},{url:"/_offline",revision:"MXmK67eIRgmHH81JehWbj"},{url:"/favicon.ico",revision:"07e73dfd3bc26b7124b53257ba95fc96"},{url:"/font/poppins/generator_config.txt",revision:"4dce64e8848e18c45c2fdabbe045335d"},{url:"/font/poppins/poppins-bold-demo.html",revision:"fdd604cb0c616413d2e8b247709697bf"},{url:"/font/poppins/poppins-bold-webfont.woff",revision:"26994f9bf1a45cbd29fc49ab3ec468b5"},{url:"/font/poppins/poppins-bold-webfont.woff2",revision:"372bc341f0c4a256969b2360da7ccd48"},{url:"/font/poppins/poppins-bolditalic-demo.html",revision:"c490938b888cd890a0fd1940b1cae43b"},{url:"/font/poppins/poppins-bolditalic-webfont.woff",revision:"3194c595795ea137bd2b92f10b3a8527"},{url:"/font/poppins/poppins-bolditalic-webfont.woff2",revision:"03b130cacb4c3361816e0af8d2e2e2f0"},{url:"/font/poppins/poppins-medium-demo.html",revision:"513419a8db3123290caed074e68008a9"},{url:"/font/poppins/poppins-medium-webfont.woff",revision:"6e9d35e42eb6e7368f088be2b439122d"},{url:"/font/poppins/poppins-medium-webfont.woff2",revision:"7f3a510290336e19efd6d6329adcd318"},{url:"/font/poppins/poppins-regular-demo.html",revision:"fcc5a1e8b9ef2d3174be58ebe6f30460"},{url:"/font/poppins/poppins-regular-webfont.woff",revision:"cae665e0f4ba42dd79e01011062efd80"},{url:"/font/poppins/poppins-regular-webfont.woff2",revision:"47ed4fd3931fd63e6856fb7b60fe2563"},{url:"/font/poppins/poppins-semibold-demo.html",revision:"46fbd02bb3f3483006f95e3208aab348"},{url:"/font/poppins/poppins-semibold-webfont.woff",revision:"b5aabd34b8fc7970e925d6527749e002"},{url:"/font/poppins/poppins-semibold-webfont.woff2",revision:"82ba3cc0fcbc89006a00843228d146f3"},{url:"/font/poppins/specimen_files/grid_12-825-55-15.css",revision:"5fff298a4c23f5172c2856c4f2878a41"},{url:"/font/poppins/specimen_files/specimen_stylesheet.css",revision:"123174d5a39bdaa34a36871af61ce8ef"},{url:"/font/poppins/stylesheet.css",revision:"3f183bbe492d2dd2505087f7261e0385"},{url:"/icons/android-chrome-192x192.png",revision:"87895ca50fac59a1cd5b185366c4f172"},{url:"/icons/apple-touch-icon.png",revision:"4c3fb534390efaa18394fb6b21614a40"},{url:"/icons/favicon-16x16.png",revision:"89bdf20d01a34def5d147c6502066352"},{url:"/icons/favicon-32x32.png",revision:"9b9c6ba774d0a2ffbce04c8ce6809d87"},{url:"/icons/icon-192x192.png",revision:"60cf76038dd36878f52355f9226d182c"},{url:"/icons/icon-256x256.png",revision:"f0b3d37e6817e2c82f8d18539df4e391"},{url:"/icons/icon-384x384.png",revision:"9e4a2a170431010b6c48a42eedcad08b"},{url:"/icons/icon-512x512.png",revision:"d5499544101b8e9d0e8b17fb31748e9e"},{url:"/icons/preview-image.png",revision:"4ed6687b48e512348de6c2f3d1f6e0a8"},{url:"/icons/splashscreens-ios/ipad_splash.png",revision:"1de96ccea1da6e6154045ca5cd9f4147"},{url:"/icons/splashscreens-ios/ipadpro1_splash.png",revision:"e7b6ffff9cc4a94a778462b61aace55a"},{url:"/icons/splashscreens-ios/ipadpro2_splash.png",revision:"172076a271103aebe1102992e8757a84"},{url:"/icons/splashscreens-ios/ipadpro3_splash.png",revision:"f36b5a47f06ab2f2b8009cb30df407a4"},{url:"/icons/splashscreens-ios/iphone5_splash.png",revision:"f6e0d871ffec9f8bf43a03bbbf9b8572"},{url:"/icons/splashscreens-ios/iphone6_splash.png",revision:"0597fec2b3804c756fa43cd1f92f6d99"},{url:"/icons/splashscreens-ios/iphoneplus_splash.png",revision:"6553856a966842d555431e03c00001ff"},{url:"/icons/splashscreens-ios/iphonex_splash.png",revision:"257b5c5ba67b9b48fbe9b07870bfea63"},{url:"/icons/splashscreens-ios/iphonexr_splash.png",revision:"b9070c54467a42f99d6bddf76e50ba54"},{url:"/icons/splashscreens-ios/iphonexsmax_splash.png",revision:"7732043b5adee45544821104774f9c89"},{url:"/icons/touch-icon-ipad-retina.png",revision:"dce281626762e2bf6d0a73bd45a4780e"},{url:"/icons/touch-icon-ipad.png",revision:"231a193cee3cc49c1561cb9d4d60416d"},{url:"/icons/touch-icon-iphone-retina.png",revision:"9c8c0cc63c9b9870c56d4dce472d719f"},{url:"/icons/touch-icon-iphone.png",revision:"9586351acf5fc7a40865c7a414785cea"},{url:"/images/bike-blue-green.png",revision:"1bcb4c3a473bf51f89d5e15a0e6336ff"},{url:"/images/fietsenstalling-voorbeeld.jpg",revision:"5cb7adbd5d6953c9dac090a9eefe5e9b"},{url:"/images/icon-close-gray.png",revision:"b26bd27f9cfa12b75d5cc544c36b31d3"},{url:"/images/icon-close.png",revision:"b22e6b8e53cb259c967421f1792ab29b"},{url:"/images/icon-hamburger.png",revision:"68e958406881fd436473ee08380305f1"},{url:"/images/icon-list.png",revision:"a569ca340601ac9d50e14b791dd20229"},{url:"/images/icon-map.png",revision:"757eae24bc094e26ba4ed793c6abfb47"},{url:"/images/icon-route-white.png",revision:"d7ea65501359b6011b783b64637e93c8"},{url:"/images/icon-route.png",revision:"44d8094fc37d7bb7d9ca9a11bfbda237"},{url:"/images/kaart-voorbeeld.png",revision:"59a55cdd256c89403c37f01ee04e2bbb"},{url:"/images/logo-transparant.png",revision:"3b86c66ee4b4f9baca14f2c5d2187df2"},{url:"/images/logo-without-text.png",revision:"d44980958fadeb22800bbfbd2ea2a844"},{url:"/images/logo.png",revision:"d083244b6ff8e556199fd89d1233653e"},{url:"/manifest.json",revision:"b203f9a7549fdbb464ed2278dac40123"},{url:"/uploads/06-10-2023/media-1696587031479-756540006.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587060294-238381396.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587710713-534043846.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587760325-765443889.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587822982-56162933.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588017753-847052605.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588043664-212556406.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588052353-430698553.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588226767-587600497.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588241794-310975041.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588267445-471091990.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588343196-173624482.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696590610164-726407231.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696590732912-452327535.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696591654035-153487998.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696592235702-564845381.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696592269300-275608712.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696593037881-280251211.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696593592770-590832966.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:i,state:a})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s},{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")}),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")}),new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute((({url:e})=>!(self.origin===e.origin)),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET")})); +if(!self.define){let e,i={};const s=(s,a)=>(s=new URL(s+".js",a).href,i[s]||new Promise((i=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=i,document.head.appendChild(e)}else e=s,importScripts(s),i()})).then((()=>{let e=i[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e})));self.define=(a,n)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let c={};const r=e=>s(e,o),t={module:{uri:o},exports:c,require:r};i[o]=Promise.all(a.map((e=>t[e]||r(e)))).then((e=>(n(...e),c)))}}define(["./workbox-7c2a5a06"],(function(e){"use strict";importScripts("fallback-0Rb3hooeW91Qk7Gxnykfw.js"),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/0Rb3hooeW91Qk7Gxnykfw/_buildManifest.js",revision:"109d3ab9e86ff6149ed1dc2cc25c8a3b"},{url:"/_next/static/0Rb3hooeW91Qk7Gxnykfw/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/189-fb105318336e4380.js",revision:"fb105318336e4380"},{url:"/_next/static/chunks/265-3cec65231f171ca6.js",revision:"3cec65231f171ca6"},{url:"/_next/static/chunks/292-9f9446cee9311860.js",revision:"9f9446cee9311860"},{url:"/_next/static/chunks/714-203ccaf1187c60e1.js",revision:"203ccaf1187c60e1"},{url:"/_next/static/chunks/717-a876f1ddc5f9c556.js",revision:"a876f1ddc5f9c556"},{url:"/_next/static/chunks/748-c92bde025b91ba68.js",revision:"c92bde025b91ba68"},{url:"/_next/static/chunks/783-9e60b13972aa2555.js",revision:"9e60b13972aa2555"},{url:"/_next/static/chunks/88-bf0cd109eed7daf4.js",revision:"bf0cd109eed7daf4"},{url:"/_next/static/chunks/d7eeaac4-a1e8e8e53989db51.js",revision:"a1e8e8e53989db51"},{url:"/_next/static/chunks/d94c0b71-2242b2fc86a4947a.js",revision:"2242b2fc86a4947a"},{url:"/_next/static/chunks/dd81a582-41786d5833b74cce.js",revision:"41786d5833b74cce"},{url:"/_next/static/chunks/framework-63157d71ad419e09.js",revision:"63157d71ad419e09"},{url:"/_next/static/chunks/main-99f5e3f92a717c61.js",revision:"99f5e3f92a717c61"},{url:"/_next/static/chunks/pages/%5BurlName%5D-3cc12e80d56e8a36.js",revision:"3cc12e80d56e8a36"},{url:"/_next/static/chunks/pages/%5BurlName%5D/%5BpageName%5D-712342798917429f.js",revision:"712342798917429f"},{url:"/_next/static/chunks/pages/_app-c7ae4465be87fcfb.js",revision:"c7ae4465be87fcfb"},{url:"/_next/static/chunks/pages/_error-54de1933a164a1ff.js",revision:"54de1933a164a1ff"},{url:"/_next/static/chunks/pages/_offline-c64183d4136009ce.js",revision:"c64183d4136009ce"},{url:"/_next/static/chunks/pages/content-a13780494f18049e.js",revision:"a13780494f18049e"},{url:"/_next/static/chunks/pages/filter-9c14b224bd0b2bd0.js",revision:"9c14b224bd0b2bd0"},{url:"/_next/static/chunks/pages/index-9cd05a30dd9d09af.js",revision:"9cd05a30dd9d09af"},{url:"/_next/static/chunks/pages/loading-0cbccf288848fdff.js",revision:"0cbccf288848fdff"},{url:"/_next/static/chunks/pages/login-b97d46486c463fbb.js",revision:"b97d46486c463fbb"},{url:"/_next/static/chunks/pages/login/error-09487df76a55db67.js",revision:"09487df76a55db67"},{url:"/_next/static/chunks/pages/parking-facilities-6f6e5ac186bcabc8.js",revision:"6f6e5ac186bcabc8"},{url:"/_next/static/chunks/pages/stalling/%5Bid%5D-5ad02c7ec8515633.js",revision:"5ad02c7ec8515633"},{url:"/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js",revision:"79330112775102f91e1010318bae2bd3"},{url:"/_next/static/chunks/webpack-f1c64600e64f11be.js",revision:"f1c64600e64f11be"},{url:"/_next/static/css/00841263f4ca408b.css",revision:"00841263f4ca408b"},{url:"/_next/static/css/2ba05e4ff1f637dc.css",revision:"2ba05e4ff1f637dc"},{url:"/_next/static/css/448f8af27fbceac0.css",revision:"448f8af27fbceac0"},{url:"/_next/static/css/476c43bd2ea5ebeb.css",revision:"476c43bd2ea5ebeb"},{url:"/_next/static/css/4b424eaa5ffd81a5.css",revision:"4b424eaa5ffd81a5"},{url:"/_next/static/css/51fea7c636f88f12.css",revision:"51fea7c636f88f12"},{url:"/_next/static/css/963c15b87a85cc54.css",revision:"963c15b87a85cc54"},{url:"/_next/static/css/be83c9cb059e752e.css",revision:"be83c9cb059e752e"},{url:"/_next/static/css/ee5279d39fad2a37.css",revision:"ee5279d39fad2a37"},{url:"/_next/static/media/roboto-all-300-normal.39add8fb.woff",revision:"39add8fb"},{url:"/_next/static/media/roboto-all-400-normal.2e9e9400.woff",revision:"2e9e9400"},{url:"/_next/static/media/roboto-all-500-normal.d96daa81.woff",revision:"d96daa81"},{url:"/_next/static/media/roboto-all-700-normal.ca3d0fdb.woff",revision:"ca3d0fdb"},{url:"/_next/static/media/roboto-cyrillic-300-normal.88798412.woff2",revision:"88798412"},{url:"/_next/static/media/roboto-cyrillic-400-normal.2d9c9d60.woff2",revision:"2d9c9d60"},{url:"/_next/static/media/roboto-cyrillic-500-normal.aa68ea54.woff2",revision:"aa68ea54"},{url:"/_next/static/media/roboto-cyrillic-700-normal.258a358e.woff2",revision:"258a358e"},{url:"/_next/static/media/roboto-cyrillic-ext-300-normal.cd7c5715.woff2",revision:"cd7c5715"},{url:"/_next/static/media/roboto-cyrillic-ext-400-normal.d7827ae3.woff2",revision:"d7827ae3"},{url:"/_next/static/media/roboto-cyrillic-ext-500-normal.a1b5c90d.woff2",revision:"a1b5c90d"},{url:"/_next/static/media/roboto-cyrillic-ext-700-normal.dd3651fb.woff2",revision:"dd3651fb"},{url:"/_next/static/media/roboto-greek-300-normal.25dc89b0.woff2",revision:"25dc89b0"},{url:"/_next/static/media/roboto-greek-400-normal.63e6dc18.woff2",revision:"63e6dc18"},{url:"/_next/static/media/roboto-greek-500-normal.533b03d2.woff2",revision:"533b03d2"},{url:"/_next/static/media/roboto-greek-700-normal.432b858b.woff2",revision:"432b858b"},{url:"/_next/static/media/roboto-greek-ext-300-normal.bc5ce703.woff2",revision:"bc5ce703"},{url:"/_next/static/media/roboto-greek-ext-400-normal.2b547ded.woff2",revision:"2b547ded"},{url:"/_next/static/media/roboto-greek-ext-500-normal.7ea6cffa.woff2",revision:"7ea6cffa"},{url:"/_next/static/media/roboto-greek-ext-700-normal.a8d16efd.woff2",revision:"a8d16efd"},{url:"/_next/static/media/roboto-latin-300-normal.a4eae32d.woff2",revision:"a4eae32d"},{url:"/_next/static/media/roboto-latin-400-normal.f2894edc.woff2",revision:"f2894edc"},{url:"/_next/static/media/roboto-latin-500-normal.3170fd9a.woff2",revision:"3170fd9a"},{url:"/_next/static/media/roboto-latin-700-normal.71b2beb8.woff2",revision:"71b2beb8"},{url:"/_next/static/media/roboto-latin-ext-300-normal.37d4965d.woff2",revision:"37d4965d"},{url:"/_next/static/media/roboto-latin-ext-400-normal.21abc8c8.woff2",revision:"21abc8c8"},{url:"/_next/static/media/roboto-latin-ext-500-normal.85ebfb55.woff2",revision:"85ebfb55"},{url:"/_next/static/media/roboto-latin-ext-700-normal.6af98c24.woff2",revision:"6af98c24"},{url:"/_next/static/media/roboto-vietnamese-300-normal.b3d3e960.woff2",revision:"b3d3e960"},{url:"/_next/static/media/roboto-vietnamese-400-normal.c95fc061.woff2",revision:"c95fc061"},{url:"/_next/static/media/roboto-vietnamese-500-normal.7f8c0554.woff2",revision:"7f8c0554"},{url:"/_next/static/media/roboto-vietnamese-700-normal.72bf832f.woff2",revision:"72bf832f"},{url:"/_offline",revision:"0Rb3hooeW91Qk7Gxnykfw"},{url:"/favicon.ico",revision:"07e73dfd3bc26b7124b53257ba95fc96"},{url:"/font/poppins/generator_config.txt",revision:"4dce64e8848e18c45c2fdabbe045335d"},{url:"/font/poppins/poppins-bold-demo.html",revision:"fdd604cb0c616413d2e8b247709697bf"},{url:"/font/poppins/poppins-bold-webfont.woff",revision:"26994f9bf1a45cbd29fc49ab3ec468b5"},{url:"/font/poppins/poppins-bold-webfont.woff2",revision:"372bc341f0c4a256969b2360da7ccd48"},{url:"/font/poppins/poppins-bolditalic-demo.html",revision:"c490938b888cd890a0fd1940b1cae43b"},{url:"/font/poppins/poppins-bolditalic-webfont.woff",revision:"3194c595795ea137bd2b92f10b3a8527"},{url:"/font/poppins/poppins-bolditalic-webfont.woff2",revision:"03b130cacb4c3361816e0af8d2e2e2f0"},{url:"/font/poppins/poppins-medium-demo.html",revision:"513419a8db3123290caed074e68008a9"},{url:"/font/poppins/poppins-medium-webfont.woff",revision:"6e9d35e42eb6e7368f088be2b439122d"},{url:"/font/poppins/poppins-medium-webfont.woff2",revision:"7f3a510290336e19efd6d6329adcd318"},{url:"/font/poppins/poppins-regular-demo.html",revision:"fcc5a1e8b9ef2d3174be58ebe6f30460"},{url:"/font/poppins/poppins-regular-webfont.woff",revision:"cae665e0f4ba42dd79e01011062efd80"},{url:"/font/poppins/poppins-regular-webfont.woff2",revision:"47ed4fd3931fd63e6856fb7b60fe2563"},{url:"/font/poppins/poppins-semibold-demo.html",revision:"46fbd02bb3f3483006f95e3208aab348"},{url:"/font/poppins/poppins-semibold-webfont.woff",revision:"b5aabd34b8fc7970e925d6527749e002"},{url:"/font/poppins/poppins-semibold-webfont.woff2",revision:"82ba3cc0fcbc89006a00843228d146f3"},{url:"/font/poppins/specimen_files/grid_12-825-55-15.css",revision:"5fff298a4c23f5172c2856c4f2878a41"},{url:"/font/poppins/specimen_files/specimen_stylesheet.css",revision:"123174d5a39bdaa34a36871af61ce8ef"},{url:"/font/poppins/stylesheet.css",revision:"3f183bbe492d2dd2505087f7261e0385"},{url:"/icons/android-chrome-192x192.png",revision:"87895ca50fac59a1cd5b185366c4f172"},{url:"/icons/apple-touch-icon.png",revision:"4c3fb534390efaa18394fb6b21614a40"},{url:"/icons/favicon-16x16.png",revision:"89bdf20d01a34def5d147c6502066352"},{url:"/icons/favicon-32x32.png",revision:"9b9c6ba774d0a2ffbce04c8ce6809d87"},{url:"/icons/icon-192x192.png",revision:"60cf76038dd36878f52355f9226d182c"},{url:"/icons/icon-256x256.png",revision:"f0b3d37e6817e2c82f8d18539df4e391"},{url:"/icons/icon-384x384.png",revision:"9e4a2a170431010b6c48a42eedcad08b"},{url:"/icons/icon-512x512.png",revision:"d5499544101b8e9d0e8b17fb31748e9e"},{url:"/icons/preview-image.png",revision:"4ed6687b48e512348de6c2f3d1f6e0a8"},{url:"/icons/splashscreens-ios/ipad_splash.png",revision:"1de96ccea1da6e6154045ca5cd9f4147"},{url:"/icons/splashscreens-ios/ipadpro1_splash.png",revision:"e7b6ffff9cc4a94a778462b61aace55a"},{url:"/icons/splashscreens-ios/ipadpro2_splash.png",revision:"172076a271103aebe1102992e8757a84"},{url:"/icons/splashscreens-ios/ipadpro3_splash.png",revision:"f36b5a47f06ab2f2b8009cb30df407a4"},{url:"/icons/splashscreens-ios/iphone5_splash.png",revision:"f6e0d871ffec9f8bf43a03bbbf9b8572"},{url:"/icons/splashscreens-ios/iphone6_splash.png",revision:"0597fec2b3804c756fa43cd1f92f6d99"},{url:"/icons/splashscreens-ios/iphoneplus_splash.png",revision:"6553856a966842d555431e03c00001ff"},{url:"/icons/splashscreens-ios/iphonex_splash.png",revision:"257b5c5ba67b9b48fbe9b07870bfea63"},{url:"/icons/splashscreens-ios/iphonexr_splash.png",revision:"b9070c54467a42f99d6bddf76e50ba54"},{url:"/icons/splashscreens-ios/iphonexsmax_splash.png",revision:"7732043b5adee45544821104774f9c89"},{url:"/icons/touch-icon-ipad-retina.png",revision:"dce281626762e2bf6d0a73bd45a4780e"},{url:"/icons/touch-icon-ipad.png",revision:"231a193cee3cc49c1561cb9d4d60416d"},{url:"/icons/touch-icon-iphone-retina.png",revision:"9c8c0cc63c9b9870c56d4dce472d719f"},{url:"/icons/touch-icon-iphone.png",revision:"9586351acf5fc7a40865c7a414785cea"},{url:"/images/bike-blue-green.png",revision:"1bcb4c3a473bf51f89d5e15a0e6336ff"},{url:"/images/fietsenstalling-voorbeeld.jpg",revision:"5cb7adbd5d6953c9dac090a9eefe5e9b"},{url:"/images/icon-close-gray.png",revision:"b26bd27f9cfa12b75d5cc544c36b31d3"},{url:"/images/icon-close.png",revision:"b22e6b8e53cb259c967421f1792ab29b"},{url:"/images/icon-hamburger.png",revision:"68e958406881fd436473ee08380305f1"},{url:"/images/icon-list.png",revision:"a569ca340601ac9d50e14b791dd20229"},{url:"/images/icon-map.png",revision:"757eae24bc094e26ba4ed793c6abfb47"},{url:"/images/icon-route-white.png",revision:"d7ea65501359b6011b783b64637e93c8"},{url:"/images/icon-route.png",revision:"44d8094fc37d7bb7d9ca9a11bfbda237"},{url:"/images/kaart-voorbeeld.png",revision:"59a55cdd256c89403c37f01ee04e2bbb"},{url:"/images/logo-transparant.png",revision:"3b86c66ee4b4f9baca14f2c5d2187df2"},{url:"/images/logo-without-text.png",revision:"d44980958fadeb22800bbfbd2ea2a844"},{url:"/images/logo.png",revision:"d083244b6ff8e556199fd89d1233653e"},{url:"/manifest.json",revision:"b203f9a7549fdbb464ed2278dac40123"},{url:"/uploads/06-10-2023/media-1696587031479-756540006.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587060294-238381396.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587710713-534043846.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587760325-765443889.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696587822982-56162933.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588017753-847052605.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588043664-212556406.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588052353-430698553.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588226767-587600497.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588241794-310975041.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588267445-471091990.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696588343196-173624482.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696590610164-726407231.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696590732912-452327535.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696591654035-153487998.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696592235702-564845381.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696592269300-275608712.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696593037881-280251211.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"},{url:"/uploads/06-10-2023/media-1696593592770-590832966.jpeg",revision:"186bea7a148312d4fdc4ff61e3e10013"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:i,event:s,state:a})=>i&&"opaqueredirect"===i.type?new Response(i.body,{status:200,statusText:"OK",headers:i.headers}):i},{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;const i=e.pathname;return!i.startsWith("/api/auth/")&&!!i.startsWith("/api/")}),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")}),new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET"),e.registerRoute((({url:e})=>!(self.origin===e.origin)),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600}),{handlerDidError:async({request:e})=>self.fallback(e)}]}),"GET")})); diff --git a/src/components/FilterBox.tsx b/src/components/FilterBox.tsx index a175780..64ecca5 100644 --- a/src/components/FilterBox.tsx +++ b/src/components/FilterBox.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { FiFilter } from "react-icons/fi"; +import { useSession } from "next-auth/react"; import { AiFillMinusCircle, AiFillPlusCircle } from "react-icons/ai"; import FilterBoxList, { updateActiveTypeStates, @@ -10,7 +11,7 @@ import FilterBoxPrice, { updateActivePriceStates, } from "~/components/FilterBoxPrice"; import { useDispatch, useSelector } from "react-redux"; -import { toggleType } from "~/store/filterSlice"; +import { toggleType, toggleType2 } from "~/store/filterSlice"; import { AppState } from "~/store/store"; const OPTIONS_1 = [ @@ -67,6 +68,15 @@ const OPTIONS_PRICE = [ { id: "jaarabonnement", title: "Jaarabonnement", active: true }, ]; +const OPTIONS_SUBMISSIONS = [ + { + id: "show_submissions", + name: "show_submissions", + title: "Toon Aanmeldingen", + active: false, + }, +]; + type FilterBoxProps = { children?: React.ReactNode; isOpen: boolean; @@ -89,6 +99,7 @@ const FilterBox: React.FC = ({ onReset: Function; }) => { const dispatch = useDispatch(); + const { data: session } = useSession() const activeTypes = useSelector( (state: AppState) => state.filter.activeTypes @@ -99,15 +110,24 @@ const FilterBox: React.FC = ({ dispatch(toggleType({ pfType: optionId })); }; + const activeTypes2 = useSelector( + (state: AppState) => state.filter.activeTypes2 + ); + + const toggleFilter2 = (optionId: string) => { + // ("toggleFilter", optionId); + dispatch(toggleType2({ pfType: optionId })); + }; + const options1_with_state = updateActiveTypeStates(OPTIONS_1, activeTypes); const options2_with_state = updateActiveTypeStates(OPTIONS_2, activeTypes); const options_price_with_state = updateActivePriceStates(OPTIONS_PRICE, []); + const options3_with_state = updateActiveTypeStates(OPTIONS_SUBMISSIONS, activeTypes2); return (
= ({ />
+ {session && session.user ? +
+ +
: null} + {children}
diff --git a/src/components/Form/FormCheckbox.tsx b/src/components/Form/FormCheckbox.tsx index f40a9eb..4632a06 100644 --- a/src/components/Form/FormCheckbox.tsx +++ b/src/components/Form/FormCheckbox.tsx @@ -13,7 +13,7 @@ function FormCheckbox({ classes?: string, checked?: boolean, defaultChecked?: boolean, - onChange?: Function, + onChange?: React.ChangeEventHandler | undefined, children?: any, }) { return ( @@ -25,7 +25,7 @@ function FormCheckbox({ ${classes} `} > - (onChange ? onChange(e) : () => {} )} + onChange={(e) => (onChange ? onChange(e) : () => { })} /> {children} diff --git a/src/components/MapComponent.tsx b/src/components/MapComponent.tsx index 1afdf98..dcdafae 100644 --- a/src/components/MapComponent.tsx +++ b/src/components/MapComponent.tsx @@ -104,6 +104,8 @@ function MapboxMap({ fietsenstallingen = [] }: any) { const filterQuery = useSelector((state: AppState) => state.filter.query); + const filterTypes2 = useSelector((state: AppState) => state.filter.activeTypes2); + const mapExtent = useSelector((state: AppState) => state.map.extent); const mapZoom = useSelector((state: AppState) => state.map.zoom); @@ -340,6 +342,7 @@ function MapboxMap({ fietsenstallingen = [] }: any) { "all", ["in", ["get", "type"], ["literal", filterActiveTypes]], ]; + if (filterQuery === "") { filter = [ "all", @@ -385,7 +388,7 @@ function MapboxMap({ fietsenstallingen = [] }: any) { } catch (ex) { console.warn("error in MapComponent layer setfilter useEffect call", ex); } - }, [stateMap, fietsenstallingen, filterActiveTypes, filterQuery]); + }, [stateMap, fietsenstallingen, filterActiveTypes, filterQuery, filterTypes2]); // Update visible features in state if filter changes React.useEffect(() => { diff --git a/src/components/Parking.tsx b/src/components/Parking.tsx index 6ad43ba..8c5cc4d 100644 --- a/src/components/Parking.tsx +++ b/src/components/Parking.tsx @@ -1,65 +1,81 @@ import React, { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; +import { useRouter } from 'next/router' import { type ParkingDetailsType } from "~/types/"; -import { getParkingDetails, generateRandomId } from "~/utils/parkings"; +import { getParkingDetails, getNewStallingDefaultRecord } from "~/utils/parkings"; import ParkingEdit from "~/components/parking/ParkingEdit"; import ParkingView from "~/components/parking/ParkingView"; -const Parking = ({ - parkingID, - startInEditMode = false -}: { - parkingID: string, - startInEditMode?: boolean -}) => { +const Parking = () => { const session = useSession(); + const router = useRouter(); - const [currentStalling, setCurrentStalling] = useState(null); const [currentRevision, setCurrentRevision] = useState(0); + const [currentStalling, setCurrentStalling] = useState(null); + const [editMode, setEditMode] = React.useState(false); useEffect(() => { - const stallingId = parkingID; + if (router.query.stallingid === undefined || Array.isArray(router.query.stallingid)) { + return; + } + + const stallingId = router.query.stallingid; if (stallingId === undefined || Array.isArray(stallingId)) { console.warn('stallingId is undefined or array', stallingId); return; } - if (stallingId === "nieuw") { - console.warn('edit of stallingid "nieuw" is not allowed'); - return; + if (stallingId === "aanmelden") { + router.replace({ query: {} }, undefined, { shallow: true }); + + let prefix = ''; + if (!session) { + // when no user is logged in, a recognizalbe prefix is used + prefix = 'VOORSTEL'; + } + setCurrentStalling(getNewStallingDefaultRecord("")); + setEditMode(true); + } else { + getParkingDetails(stallingId).then((stalling) => { + if (null !== stalling) { + setCurrentStalling(stalling); + } + }); } // console.log(`***** getParkingDetails ${stallingId} -R ${currentRevision} ******`); - getParkingDetails(stallingId).then((stalling) => { - setCurrentStalling(stalling); - }); }, [ - parkingID, + router.query.stallingid, currentRevision ]); - const handleCloseEdit = () => { - // console.log("handleCloseEdit"); - setEditMode(false); + const handleCloseEdit = (changeStallingID?: string) => { + if (changeStallingID) { + router.replace({ query: { stallingid: changeStallingID } }, undefined, { shallow: true }); + getParkingDetails(changeStallingID).then((stalling) => { + if (null !== stalling) { + setCurrentStalling(stalling); + setEditMode(false); + } + }); + } else { + setEditMode(false); + } } const handleUpdateRevision = () => { setCurrentRevision(currentRevision + 1); } - const [editMode, setEditMode] = React.useState(startInEditMode); - - const allowEdit = session.status === "authenticated" || parkingID.substring(0, 8) === "VOORSTEL"; - + const allowEdit = session.status === "authenticated" || currentStalling && currentStalling.ID === ""; if (null === currentStalling) { - return (null); + return null; } - if (allowEdit === true && (editMode === true)) { - return ( handleCloseEdit()} onChange={handleUpdateRevision} />); + return (); } else { return ( { setEditMode(true) } : undefined} />); } diff --git a/src/components/ParkingFacilities.tsx b/src/components/ParkingFacilities.tsx index 2f82ecf..1ef6be5 100644 --- a/src/components/ParkingFacilities.tsx +++ b/src/components/ParkingFacilities.tsx @@ -10,6 +10,7 @@ import CardList from "~/components/CardList"; import { CardData } from "~/components/Card"; import FilterBox from "~/components/FilterBox"; import FooterNav from "~/components/FooterNav"; +import { ParkingDetailsType } from "~/types"; const ParkingFacilities = ({ fietsenstallingen, @@ -21,10 +22,16 @@ const ParkingFacilities = ({ const activeTypes = useSelector( (state: AppState) => state.filter.activeTypes ); + const filterQuery = useSelector( + (state: AppState) => state.filter.query + ); + const activeTypes2 = useSelector( + (state: AppState) => state.filter.activeTypes2 + ); - const toggleParkingFacilitiesView = () => setMapmode(!mapmode); + // const toggleParkingFacilitiesView = () => setMapmode(!mapmode); const toggleFilterBox = () => setIsFilterBoxOpen(!isFilterBoxOpen); - const resetFilter = () => {}; + const resetFilter = () => { }; // let cards: CardData[] = []; let filteredFietsenstallingen: any[] = []; @@ -38,10 +45,15 @@ const ParkingFacilities = ({ // description: x.Description, // }; // }); - - filteredFietsenstallingen = fietsenstallingen.filter( - (x: any) => activeTypes.indexOf(x.Type) > -1 - ); + if (activeTypes2 && activeTypes2.includes("show_submissions")) { + filteredFietsenstallingen = fietsenstallingen.filter( + (x: any) => (x.ID.substring(0, 8) === "VOORSTEL") + ); + } else { + filteredFietsenstallingen = fietsenstallingen.filter( + (x: any) => (x.ID.substring(0, 8) !== "VOORSTEL") + ); + } } return ( diff --git a/src/components/ParkingFacilityBrowser.tsx b/src/components/ParkingFacilityBrowser.tsx index 430f562..8cd8ae8 100644 --- a/src/components/ParkingFacilityBrowser.tsx +++ b/src/components/ParkingFacilityBrowser.tsx @@ -4,8 +4,10 @@ import { useDispatch, useSelector } from "react-redux"; import { setSelectedParkingId, setInitialLatLng } from "~/store/mapSlice"; import { setQuery } from "~/store/filterSlice"; import { setMunicipalities } from "~/store/geoSlice"; +import { useSession } from "next-auth/react"; import { convertCoordinatenToCoords } from "~/utils/map/index"; +import type { fietsenstallingen } from "@prisma/client"; import { getMunicipalities @@ -13,14 +15,16 @@ import { import ParkingFacilityBrowserStyles from './ParkingFacilityBrowser.module.css'; -import Input from "@mui/material/TextField"; import SearchBar from "~/components/SearchBar"; import ParkingFacilityBlock from "~/components/ParkingFacilityBlock"; +import type { AppState } from "~/store/store"; + + const MunicipalityBlock = ({ title, onClick -}) => { +}: { title: string, onClick: React.MouseEventHandler }) => { return (
void; - showSearchBar?: false - customFilter?: Function + showSearchBar?: boolean; + customFilter?: Function; }) { const dispatch = useDispatch(); + const session = useSession(); const [visibleParkings, setVisibleParkings] = useState(fietsenstallingen); const [visibleMunicipalities, setVisibleMunicipalities] = useState([]); @@ -76,6 +81,10 @@ function ParkingFacilityBrowser({ (state: AppState) => state.map.selectedParkingId ); + const filterTypes2 = useSelector( + (state: AppState) => state.filter.activeTypes2 + ); + const filterQuery = useSelector( (state: AppState) => state.filter.query ); @@ -92,9 +101,28 @@ function ParkingFacilityBrowser({ if (!filterQuery && !mapVisibleFeatures) return; // Create variable that represents all parkings - const allParkings = fietsenstallingen; + let allParkings = fietsenstallingen; + if (filterTypes2 && filterTypes2.includes('show_submissions')) { + allParkings = allParkings.filter((x) => { + // console.log("filter-is", x.ID, x.ID.substring(0, 8), x.ID.substring(0, 7) !== 'VOORSTEL') + return x.ID.substring(0, 8) === 'VOORSTEL' + }); + } else { + allParkings = allParkings.filter((x) => { + // console.log("filter-is", x.ID, x.ID.substring(0, 8), x.ID.substring(0, 7) !== 'VOORSTEL') + return x.ID.substring(0, 8) !== 'VOORSTEL' + }); + } + // Create variable that will have the filtered parkings let filtered = allParkings; + + // show all submissions and no other parkings + if (filterTypes2 && filterTypes2.includes('show_submissions')) { + setVisibleParkings(filtered); + return; + } + // If custom filter is given: Only apply custom filter if (customFilter) { filtered = filtered.filter((x) => { @@ -207,7 +235,8 @@ function ParkingFacilityBrowser({ mapVisibleFeatures, mapVisibleFeatures.length, filterQuery, - customFilter + customFilter, + filterTypes2, ]); useEffect(() => { @@ -282,9 +311,9 @@ function ParkingFacilityBrowser({ overflow: "auto", }} > - {showSearchBar ? { + filterChanged={(e: { target: { value: any; }; }) => { dispatch(setQuery(e.target.value)) }} /> : ''} diff --git a/src/components/parking/ParkingEdit.tsx b/src/components/parking/ParkingEdit.tsx index c87a761..129aed1 100644 --- a/src/components/parking/ParkingEdit.tsx +++ b/src/components/parking/ParkingEdit.tsx @@ -1,106 +1,114 @@ -import React, { ReactEventHandler } from "react"; - -import { openRoute } from "~/utils/map/index"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; +import React from "react"; // Import components import PageTitle from "~/components/PageTitle"; -import ImageSlider from "~/components/ImageSlider"; import HorizontalDivider from "~/components/HorizontalDivider"; import { Button, IconButton } from "~/components/Button"; import FormInput from "~/components/Form/FormInput"; -import FormCheckbox from "~/components/Form/FormCheckbox"; import SectionBlock from "~/components/SectionBlock"; import SectionBlockEdit from "~/components/SectionBlockEdit"; -import type { ParkingDetailsType } from "~/types/"; +import type { ParkingDetailsType, ParkingSections } from "~/types/"; import { - getAllServices + getAllServices, + generateRandomId, } from "~/utils/parkings"; import { Tabs, Tab, FormHelperText, FormLabel, Typography } from "@mui/material"; /* Use nicely formatted items for items that can not be changed yet */ import ParkingViewTarief from "~/components/parking/ParkingViewTarief"; -import ParkingViewCapaciteit from "~/components/parking/ParkingViewCapaciteit"; import ParkingViewAbonnementen from "~/components/parking/ParkingViewAbonnementen"; -import ParkingEditCapaciteit from "~/components/parking/ParkingEditCapaciteit"; +import ParkingEditCapaciteit, { type CapaciteitType } from "~/components/parking/ParkingEditCapaciteit"; import ParkingEditLocation from "~/components/parking/ParkingEditLocation"; import ParkingEditAfbeelding from "~/components/parking/ParkingEditAfbeelding"; -import ParkingEditOpening, {type OpeningChangedType} from "~/components/parking/ParkingEditOpening"; +import ParkingEditOpening, { type OpeningChangedType } from "~/components/parking/ParkingEditOpening"; +import { useSession } from "next-auth/react" + +export type ParkingEditUpdateStructure = { + ID?: string; + Title?: string; + Location?: string; + Postcode?: string; + Plaats?: string; + Coordinaten?: string; + Type?: string; + // [key: string]: string | undefined; + Openingstijden?: any; // Replace with the actual type if different + fietsenstalling_secties?: ParkingSections; // Replace with the actual type if different +} + +type ServiceType = { ID: string, Name: string }; +type ChangedType = { ID: string, selected: boolean }; const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingDetailsType, onClose: Function, onChange: Function }) => { - const router = useRouter(); - const session = useSession(); const [selectedTab, setSelectedTab] = React.useState('tab-algemeen'); // const [selectedTab, setSelectedTab] = React.useState('tab-capaciteit'); - const [newTitle, setNewTitle ] = React.useState(undefined); - const [newLocation, setNewLocation ] = React.useState(undefined); - const [newPostcode, setNewPostcode ] = React.useState(undefined); - const [newPlaats, setNewPlaats ] = React.useState(undefined); - const [newCoordinaten, setNewCoordinaten ] = React.useState(undefined); + const [newTitle, setNewTitle] = React.useState(undefined); + const [newLocation, setNewLocation] = React.useState(undefined); + const [newPostcode, setNewPostcode] = React.useState(undefined); + const [newPlaats, setNewPlaats] = React.useState(undefined); + const [newCoordinaten, setNewCoordinaten] = React.useState(undefined); // used for map recenter when coordinates are manually changed - const [centerCoords, setCenterCoords ] = React.useState(undefined); + const [centerCoords, setCenterCoords] = React.useState(undefined); - type FietsenstallingSectiesType = { [key: string]: Array[] } + // type FietsenstallingSectiesType = { [key: string]: Array[] } - type ServiceType = { ID: string, Name: string}; - type ChangedType = { ID: string, selected: boolean}; + const [allServices, setAllServices] = React.useState([]); + const [newServices, setNewServices] = React.useState([]); - const [allServices, setAllServices ] = React.useState([]); - const [newServices, setNewServices ] = React.useState([]); + const [newCapaciteit, setNewCapaciteit] = React.useState([]); // capaciteitschema + const [newOpening, setNewOpening] = React.useState(undefined); // openingstijdenschema + const [newOpeningstijden, setNewOpeningstijden] = React.useState(undefined); // textveld afwijkende openingstijden - const [newCapaciteit, setNewCapaciteit ] = React.useState([]); // capaciteitschema - const [newOpening, setNewOpening ] = React.useState(undefined); // openingstijdenschema - const [newOpeningstijden, setNewOpeningstijden ] = React.useState(undefined); // textveld afwijkende openingstijden + type StallingType = { id: string, name: string, sequence: number }; + const [allTypes, setAllTypes] = React.useState([]); + const [newStallingType, setNewStallingType] = React.useState(undefined); - type StallingType = { id: string, name: string, sequence: number}; - const [allTypes, setAllTypes ] = React.useState([]); - const [newStallingType, setNewStallingType ] = React.useState(undefined); + const { data: session } = useSession() // Set 'allServices' variable in local state React.useEffect(() => { (async () => { const result = await getAllServices(); setAllServices(result); - })(); - },[]) + })(); + }, []) React.useEffect(() => { (async () => { try { const response = await fetch( - `/api/fietsenstallingtypen/` - ); + `/api/fietsenstallingtypen/` + ); const json = await response.json(); - if(! json) return; + if (!json) return; setAllTypes(json); - } catch(err) { + } catch (err) { console.error("get all types error", err); } - })(); - },[]) + })(); + }, []) const handleChange = (event: React.SyntheticEvent, newValue: string) => { setSelectedTab(newValue); } - - const getUpdate = (): any => { - let update: any = {}; - - if(newTitle !== undefined) { update.Title = newTitle; } - if(newLocation !== undefined) { update.Location = newLocation; } - if(newPostcode !== undefined) { update.Postcode = newPostcode; } - if(newPlaats !== undefined) { update.Plaats = newPlaats; } - if(newCoordinaten !== undefined) { update.Coordinaten = newCoordinaten; } - if(newStallingType !== undefined) { update.Type = newStallingType; } - - if(undefined!==newOpening) { - for(const keystr in newOpening) { - const key = keystr as keyof ParkingDetailsType; + + const getUpdate = () => { + let update: ParkingEditUpdateStructure = {}; + + if (newTitle !== undefined) { update.Title = newTitle; } + if (newLocation !== undefined) { update.Location = newLocation; } + if (newPostcode !== undefined) { update.Postcode = newPostcode; } + if (newPlaats !== undefined) { update.Plaats = newPlaats; } + if (newCoordinaten !== undefined) { update.Coordinaten = newCoordinaten; } + if (newStallingType !== undefined) { update.Type = newStallingType; } + + if (undefined !== newOpening) { + for (const keystr in newOpening) { + const key = keystr as keyof ParkingEditUpdateStructure; update[key] = new Date(newOpening[key]).toISOString(); } } @@ -112,17 +120,16 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD // } // console.log('>> update', update) - if(undefined!==newOpeningstijden) { - if(newOpeningstijden !== parkingdata.Openingstijden) { + if (undefined !== newOpeningstijden) { + if (newOpeningstijden !== parkingdata.Openingstijden) { update.Openingstijden = newOpeningstijden; } } - // console.log("got update", JSON.stringify(update,null,2)); return update; } - const updateServices = async (parkingdata, newServices) => { + const updateServices = async (parkingdata: ParkingDetailsType, newServices: ChangedType[]) => { try { // Delete existing services for this parking await fetch( @@ -134,18 +141,18 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD // - First, add existing services parkingdata.fietsenstallingen_services.forEach(x => { // To be removed? - const doRemove = newServices.filter(s => s.ID === x.services.ID && ! s.selected).pop(); - if(! doRemove) { + const doRemove = newServices.filter(s => s.ID === x.services.ID && !s.selected).pop(); + if (!doRemove) { servicesToSave.push({ ServiceID: x.services.ID, FietsenstallingID: parkingdata.ID, - }); + }); } }) // - Second, add new services newServices.forEach(s => { // Don't add if service is not selected - if(! s.selected) return; + if (!s.selected) return; servicesToSave.push({ ServiceID: s.ID, @@ -163,20 +170,20 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD }, } ); - } catch(err) { + } catch (err) { console.error(err); } } - const updateCapaciteit = async (parkingdata, newCapaciteit) => { - if(! newCapaciteit || newCapaciteit.length <= 0) return; - console.log('parkingdata, newCapaciteit', parkingdata, newCapaciteit) + const updateCapaciteit = async (parkingdata: ParkingDetailsType, newCapaciteit: CapaciteitType[]) => { + if (!newCapaciteit || newCapaciteit.length <= 0) return; + try { // Get section to save - const sectionToSaveResponse = await fetch('/api/fietsenstalling_sectie/findFirstByFietsenstallingsId?ID='+parkingdata.ID); + const sectionToSaveResponse = await fetch('/api/fietsenstalling_sectie/findFirstByFietsenstallingsId?ID=' + parkingdata.ID); const sectionToSave = await sectionToSaveResponse.json(); const sectionId = sectionToSave.sectieId; - + // Save capaciteit const savedCapaciteit = await fetch('/api/fietsenstalling_sectie/saveManyFromFullObject', { method: 'POST', @@ -189,31 +196,41 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD "Content-Type": "application/json", } }) - } catch(err) { + } catch (err) { console.error(err); } } - const updateParking = async () => { + const parkingChanged = () => { + console.log("PARKINGCHANGED", newServices.length > 0, newCapaciteit.length > 0, newOpening !== undefined, newOpeningstijden !== undefined); + + return Object.keys(update).length !== 0 || newServices.length > 0 || newCapaciteit.length > 0 || newOpening !== undefined || newOpeningstijden !== undefined; + } + + const handleUpdateParking = async () => { // Stop if no parking ID is available - if(! parkingdata || ! parkingdata.ID) return; + if (!parkingdata) return; + + const isNew = parkingdata.ID === ""; + if (isNew) { + parkingdata.ID = generateRandomId(session ? "" : "VOORSTEL"); + } // Check if parking was changed const update = getUpdate(); - const parkingChanged = Object.keys(update).length !== 0 || newServices.length > 0 || newCapaciteit.length > 0 || newOpening !==undefined || newOpeningstijden !== undefined; // If services are updated: Update services - if(newServices.length > 0) { + if (newServices.length > 0) { updateServices(parkingdata, newServices); } // If capaciteit is updated: Update capaciteit - if(newCapaciteit.length > 0) { + if (newCapaciteit.length > 0) { updateCapaciteit(parkingdata, newCapaciteit); } // If parking data didn't change: stop - if(!parkingChanged) { + if (!parkingChanged()) { // Go back to 'view' mode onChange(); onClose(); @@ -221,38 +238,41 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD return; } + // console.log("store changes", isNew, JSON.stringify(update)); try { + const method = isNew ? "POST" : "PUT"; + const body = JSON.stringify(isNew ? Object.assign({}, parkingdata, update) : update); const result = await fetch( "/api/fietsenstallingen?id=" + parkingdata.ID, { - method: "PUT", - body: JSON.stringify(update), + method, + body, headers: { "Content-Type": "application/json", }, } ); - if(! result.ok) { + if (!result.ok) { throw Error('Er ging iets fout bij het opslaan. Controleer of de gegevens kloppen. Is de postcode bijvoorbeeld juist, en niet te lang?') } // Go back to 'view' mode onChange(); - onClose(); - } catch(err) { - if(err.message) alert(err.message); + onClose(isNew ? parkingdata.ID : undefined); + } catch (err: any) { + if (err.message) alert(err.message); else alert(err); } }; - const update = getUpdate() - const parkingChanged = Object.keys(update).length !== 0 || newServices.length > 0 || newCapaciteit.length > 0 || newOpening !==undefined || newOpeningstijden !== undefined; + const update: ParkingEditUpdateStructure = getUpdate() + const showUpdateButtons = parkingChanged() // console.log("@@@ parkingdata", parkingdata); const updateCoordinatesFromMap = (lat: number, lng: number) => { // console.log("#### update from map") const latlngstring = `${lat},${lng}`; - if(latlngstring !== parkingdata.Coordinaten) { + if (latlngstring !== parkingdata.Coordinaten) { setNewCoordinaten(latlngstring); } else { setNewCoordinaten(undefined); @@ -264,44 +284,52 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD // console.log("#### update from form") try { const latlng = parkingdata.Coordinaten.split(","); - if(isLat) { + if (isLat) { latlng[0] = e.target.value; } else { latlng[1] = e.target.value; } setNewCoordinaten(latlng.join(",")); setCenterCoords(latlng.join(",")); - } catch(ex) { - console.warn('ParkingEditLocation - unable to set coordinates from form: ', ex.message()); + } catch (ex: any) { + if (ex.message) { + console.warn('ParkingEditLocation - unable to set coordinates from form: ', ex.message()); + } else { + console.warn('ParkingEditLocation - unable to set coordinates from form'); + } } -} + } const getCoordinate = (isLat: boolean): string => { let coords = parkingdata.Coordinaten; - if(newCoordinaten !== undefined) { + if (newCoordinaten !== undefined) { coords = newCoordinaten; - } - if(coords === "") return ""; + } + if (coords === "") return ""; const latlng = coords.split(","); - if(isLat) { - return latlng[0]?.toString()||''; + if (isLat) { + return latlng[0]?.toString() || ''; } else { - return latlng[1]?.toString()||''; + return latlng[1]?.toString() || ''; } } const renderTabAlgemeen = () => { const serviceIsActive = (ID: string): boolean => { - const change = newServices.find(s=>(s.ID===ID)); - if(change!==undefined) { + const change = newServices.find(s => (s.ID === ID)); + if (change !== undefined) { // console.log(ID, "changed to ", change.selected); return change.selected; } - for(const item of parkingdata.fietsenstallingen_services) { - if(item.services.ID===ID) { + if (undefined === parkingdata.fietsenstallingen_services) { + return false; + } + + for (const item of parkingdata.fietsenstallingen_services) { + if (item.services.ID === ID) { // console.log("selected in parkingdata", ID, change); return true; } @@ -311,11 +339,11 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD } const handleSelectService = (ID: string, checked: boolean) => { - const index = newServices.findIndex(s=>s.ID===ID); - if(index !== -1) { - newServices.splice(index, 1); + const index = newServices.findIndex(s => s.ID === ID); + if (index !== -1) { + newServices.splice(index, 1); } else { - newServices.push({ID: ID, selected: checked}); + newServices.push({ ID: ID, selected: checked }); } // console.log('newservices - after', JSON.stringify(newServices,0,2)); @@ -326,14 +354,14 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD return (
- +
{ setNewTitle(e.target.value)}} value={newTitle!==undefined?newTitle:parkingdata.Title} + onChange={e => { setNewTitle(e.target.value) }} value={newTitle !== undefined ? newTitle : parkingdata.Title} />
{ setNewLocation(e.target.value)}} value={newLocation!==undefined?newLocation:parkingdata.Location} + onChange={e => { setNewLocation(e.target.value) }} value={newLocation !== undefined ? newLocation : parkingdata.Location} />
<> - { setNewPostcode(e.target.value)}} value={newPostcode!==undefined?newPostcode:parkingdata.Postcode} /> - { setNewPlaats(e.target.value)}} value={newPlaats!==undefined?newPlaats:parkingdata.Plaats} /> + { setNewPostcode(e.target.value) }} value={newPostcode !== undefined ? newPostcode : parkingdata.Postcode} /> + { setNewPlaats(e.target.value) }} value={newPlaats !== undefined ? newPlaats : parkingdata.Plaats} />
@@ -377,10 +405,10 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD - { setNewStallingType(event.target.value) }}> {allTypes.map(type => ( ))} @@ -393,7 +421,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD
- +
Verschuif de kaart om de coordinaten aan te passen @@ -422,11 +450,11 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD } const renderTabAfbeelding = () => { - return ( + return (
- -
); - } + +
); + } const renderTabOpeningstijden = () => { const handlerSetNewOpening = (tijden: OpeningChangedType, Openingstijden: string): void => { @@ -434,7 +462,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD setNewOpeningstijden(Openingstijden); return; } - return ( + return (
{ - return ( + return (
); return (
- -
Fietsen
-
-
Eerste 24 uur:
-
gratis
-
Daarna per 24 uur:
-
€0,60
-
-
Bromfietsen
-
-
Eerste 24 uur:
-
€0,60
-
-
+ +
Fietsen
+
+
Eerste 24 uur:
+
gratis
+
Daarna per 24 uur:
+
€0,60
+
+
Bromfietsen
+
+
Eerste 24 uur:
+
€0,60
+
+
); } - + const renderTabCapaciteit = () => { - const handlerSetNewCapaciteit = (capaciteit: any): void => { + const handlerSetNewCapaciteit = (capaciteit: React.SetStateAction): void => { + console.log('handlerSetNewCapaciteit', JSON.stringify(capaciteit, null, 2)); setNewCapaciteit(capaciteit); return; } + return (
- + ); return (
- -
-
Jaarbonnement fiets
-
€80,90
-
Jaarabonnement bromfiets
-
€262.97
-
-
+ +
+
Jaarbonnement fiets
+
€80,90
+
Jaarabonnement bromfiets
+
€262.97
+
+
- {/**/} + {/**/}
); } @@ -516,18 +546,18 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD // console.log("### ParkingViewBeheerder", parkingdata, parkingdata.Exploitant, parkingdata.Beheerder, parkingdata.BeheerderContact); if (parkingdata.FMS === true) { content = FMS; - } else if(parkingdata?.exploitant) { + } else if (parkingdata?.exploitant) { content = ( - {parkingdata.exploitant.CompanyName} + {parkingdata.exploitant.CompanyName} ) - } else if(parkingdata.BeheerderContact !== null) { + } else if (parkingdata.BeheerderContact !== null) { content = ( {parkingdata.Beheerder === null ? 'contact' : parkingdata.Beheerder} - ); + ); } else { content = null } @@ -541,7 +571,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD return (
{ + onClick={(e: any) => { if (e) e.preventDefault(); - if(parkingChanged === true) { - updateParking(); - } - else { - onClose(); - } + handleUpdateParking(); }} > - { parkingChanged === true ? 'Opslaan': 'Terug' } + {showUpdateButtons === true ? 'Opslaan' : 'Terug'} - {parkingChanged === true &&
- - - + + + {/* */} - - - + + + - { selectedTab === 'tab-algemeen' ? renderTabAlgemeen() : null } - { selectedTab === 'tab-afbeelding' ? renderTabAfbeelding() : null } - { selectedTab === 'tab-openingstijden' ? renderTabOpeningstijden() : null } - { selectedTab === 'tab-tarieven' ? renderTabTarieven() : null } - { selectedTab === 'tab-capaciteit' ? renderTabCapaciteit() : null } - { selectedTab === 'tab-abonnementen' ? renderTabAbonnementen() : null } - { selectedTab === 'tab-beheerder' ? renderTabBeheerder() : null } + {selectedTab === 'tab-algemeen' ? renderTabAlgemeen() : null} + {selectedTab === 'tab-afbeelding' ? renderTabAfbeelding() : null} + {selectedTab === 'tab-openingstijden' ? renderTabOpeningstijden() : null} + {selectedTab === 'tab-tarieven' ? renderTabTarieven() : null} + {selectedTab === 'tab-capaciteit' ? renderTabCapaciteit() : null} + {selectedTab === 'tab-abonnementen' ? renderTabAbonnementen() : null} + {selectedTab === 'tab-beheerder' ? renderTabBeheerder() : null}
); }; diff --git a/src/components/parking/ParkingEditAfbeelding.tsx b/src/components/parking/ParkingEditAfbeelding.tsx index 635648d..a4c0e51 100644 --- a/src/components/parking/ParkingEditAfbeelding.tsx +++ b/src/components/parking/ParkingEditAfbeelding.tsx @@ -5,31 +5,31 @@ import { Button } from "~/components/Button"; import ImageSlider from "~/components/ImageSlider"; // based on https://codersteps.com/articles/how-to-build-a-file-uploader-with-next-js-and-formidable -const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdata: ParkingDetailsType, onUpdateAfbeelding: Function }) => { +const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdata: ParkingDetailsType, onUpdateAfbeelding?: Function }) => { const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); - const onRemoveAfbeelding = async () => { - const update = { Image: '' } - try { - const result = await fetch( - "/api/fietsenstallingen?id=" + parkingdata.ID, - { - method: "PUT", - body: JSON.stringify(update), - headers: { - "Content-Type": "application/json", - }, - } - ); - if(! result.ok) { - throw Error('Er ging iets fout bij het verwijderen. Probeer je het later nog eens.') - } - - if(onUpdateAfbeelding) { - onUpdateAfbeelding('') + const onRemoveAfbeelding = async () => { + const update = { Image: '' } + try { + const result = await fetch( + "/api/fietsenstallingen?id=" + parkingdata.ID, + { + method: "PUT", + body: JSON.stringify(update), + headers: { + "Content-Type": "application/json", + }, + } + ); + if (!result.ok) { + throw Error('Er ging iets fout bij het verwijderen. Probeer je het later nog eens.') + } + + if (onUpdateAfbeelding) { + onUpdateAfbeelding('') }; - } catch(err) { + } catch (err) { console.error('onRemoveAfbeelding - error', err); } } @@ -37,13 +37,13 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat const onFileUploadChange = (e: ChangeEvent) => { const fileInput = e.target; - if (!fileInput.files || fileInput.files.length === 0||!fileInput.files[0]) { + if (!fileInput.files || fileInput.files.length === 0 || !fileInput.files[0]) { console.warn("Files list is empty"); return; } const file = fileInput.files[0]; - + /** File validation */ if (!file.type.startsWith("image")) { alert("Ongeldig bestandstype geselecteerd. Alleen afbeeldingen zijn toegestaan."); @@ -71,7 +71,7 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat const onUploadFile = async (e: MouseEvent) => { e.preventDefault(); - if(! parkingdata || ! parkingdata.ID || !file) { + if (!parkingdata || !parkingdata.ID || !file) { return; } @@ -112,16 +112,16 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat }, } ); - if(! result.ok) { + if (!result.ok) { throw Error('Er ging iets fout bij het opslaan. Probeer je het later nog eens.') } - onUpdateAfbeelding(); - } catch(err: any) { - if(err.message) alert(err.message); + onUpdateAfbeelding && onUpdateAfbeelding(); + } catch (err: any) { + if (err.message) alert(err.message); else alert(err); } - + // onUpdateAfbeelding(data.url); } catch (error) { console.error(error); @@ -129,7 +129,7 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat } }; - if(parkingdata.Image) { + if (parkingdata.Image) { // render current image return (
@@ -137,14 +137,14 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat
+ className="mt-4 w-max-content mx-auto" + onClick={onRemoveAfbeelding} + > + Afbeelding verwijderen +
- ) - } else if(previewUrl) { + ) + } else if (previewUrl) { // render preview image return (
@@ -165,8 +165,8 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat Afbeelding gebruiken
@@ -197,9 +197,9 @@ const ParkingEditAfbeelding = ({ parkingdata, onUpdateAfbeelding }: { parkingdat type="file" onChange={onFileUploadChange} /> - +
- ); + ); } }; diff --git a/src/components/parking/ParkingEditCapaciteit.tsx b/src/components/parking/ParkingEditCapaciteit.tsx index 16c2a91..a7c51c2 100644 --- a/src/components/parking/ParkingEditCapaciteit.tsx +++ b/src/components/parking/ParkingEditCapaciteit.tsx @@ -1,22 +1,25 @@ -import React, {useEffect} from "react"; -import HorizontalDivider from "~/components/HorizontalDivider"; - -import SectionBlock from "~/components/SectionBlock"; +import React, { useEffect } from "react"; import FormInput from "~/components/Form/FormInput"; import FormCheckbox from "~/components/Form/FormCheckbox"; -import { ParkingDetailsType } from '~/types'; - +import { ParkingDetailsType, ParkingSections } from '~/types'; +import { ParkingEditUpdateStructure } from './ParkingEdit'; + import { getAllFietstypen } from "~/utils/parkings"; +import { fietstypen } from "@prisma/client"; + +export type CapaciteitType = { ID: string, Name: string }; -type capacitydata = { +export type capacitydata = { unknown: boolean; total: number; - detailed: { [key: string]: { - Toegestaan: boolean; - Capaciteit: number; - } }; + detailed: { + [key: string]: { // fietstype + Toegestaan: boolean; + Capaciteit: number; + } + }; }; const calculateCapacityData = (parking: ParkingDetailsType): capacitydata | null => { @@ -42,9 +45,12 @@ const calculateCapacityData = (parking: ParkingDetailsType): capacitydata | null } } - capacity.detailed[name].Toegestaan = capacity.detailed[name].Toegestaan || data.Toegestaan!==null && data.Toegestaan; - capacity.detailed[name].Capaciteit += data.Capaciteit||0; - capacity.total += data.Capaciteit||0; + const item = capacity.detailed[name]; + if (item) { + item.Toegestaan = item.Toegestaan || data.Toegestaan !== null && data.Toegestaan; + item.Capaciteit += data.Capaciteit || 0; + capacity.total += data.Capaciteit || 0; + } }); }); } @@ -56,67 +62,65 @@ const calculateCapacityData = (parking: ParkingDetailsType): capacitydata | null } }; -const getCapacityForFietstype = (fietstypeName, capacitydata, localChanges) => { - if(! fietstypeName) return 0; - if(! capacitydata || ! capacitydata.detailed) return 0; +const getCapacityForFietstype = (fietstypeName: string, capacitydata: capacitydata | null, localChanges: ParkingSections | undefined) => { + if (!capacitydata) return 0; + + // console.log("**** GC", JSON.stringify(localChanges, null, 2)); // First, check if we have the value in our local changes - if(localChanges) { - const foundRelated = localChanges[0].secties_fietstype.filter((x) => { - return x.fietstype.Name === fietstypeName; - }).pop(); - if(foundRelated) { - return Number(foundRelated.Capaciteit); + if (localChanges) { + let foundRelated = undefined; + if (localChanges.length > 0 && localChanges[0] !== undefined) { + foundRelated = localChanges[0].secties_fietstype.filter((x) => { + return x.fietstype.Name === fietstypeName; + }).pop(); + if (foundRelated) { + return Number(foundRelated.Capaciteit); + } } } // If not, get the original value from the database const capacityForFietstype = capacitydata.detailed[fietstypeName]; - if(capacityForFietstype && capacityForFietstype.Capaciteit) { + if (capacityForFietstype && capacityForFietstype.Capaciteit) { return Number(capacityForFietstype.Capaciteit); } return 0; } -const getAllowedValueForFietstype = (fietstypeName, capacitydata, localChanges) => { - if(! fietstypeName) return 0; - if(! capacitydata || ! capacitydata.detailed) return 0; +const getAllowedValueForFietstype = (fietstypeName: string, capacitydata: capacitydata | null, localChanges: ParkingSections | undefined): boolean => { + if (!capacitydata) return false; // First, check if we have the value in our local changes - if(localChanges) { + if (localChanges && localChanges[0]) { const foundRelated = localChanges[0].secties_fietstype.filter((x) => { return x.fietstype.Name === fietstypeName; }).pop(); - if(foundRelated) { - return foundRelated.Toegestaan; + if (foundRelated) { + return foundRelated.Toegestaan || false; } } // If not, get the original value from the database const capacityForFietstype = capacitydata.detailed[fietstypeName]; - if(capacityForFietstype && capacityForFietstype.Toegestaan) { + if (capacityForFietstype && capacityForFietstype.Toegestaan) { return true; } return false; } -const toggleActive = (fietsenstalling_secties, fietstypeName, isActive): { - fietsenstalling_secties: any, - fietstypeName: String, // 'fietstype' - isActive: Boolean // 'Toegestaan' -} => { +const toggleActive = (fietsenstalling_secties: ParkingSections, fietstypeName: string, isActive: boolean): ParkingSections => { // It's mandatory to have at least 1 section - if(! fietsenstalling_secties) return fietsenstalling_secties; - if(! fietsenstalling_secties[0]) return fietsenstalling_secties; - if(! fietsenstalling_secties[0].secties_fietstype) return fietsenstalling_secties; + if (!fietsenstalling_secties[0]) return fietsenstalling_secties; + if (!fietsenstalling_secties[0].secties_fietstype) return fietsenstalling_secties; let didUpdateSomething = false; // Update the isActive/'Toegestaan' value for the 'fietstype' given fietsenstalling_secties[0].secties_fietstype = fietsenstalling_secties[0].secties_fietstype.map(x => { - if(x.fietstype.Name === fietstypeName) { + if (x.fietstype.Name === fietstypeName) { didUpdateSomething = true; } // Update 'Toegestaan' value if needed @@ -124,12 +128,12 @@ const toggleActive = (fietsenstalling_secties, fietstypeName, isActive): { Capaciteit: x.Capaciteit, fietstype: x.fietstype, Toegestaan: x.fietstype.Name === fietstypeName - ? isActive // Update - : x.Toegestaan // Or keep existing value + ? isActive // Update + : x.Toegestaan // Or keep existing value } }); - - if(! didUpdateSomething) { + + if (!didUpdateSomething) { // If above script didn't update a value, we should add this fietstypeName const newObject = { Toegestaan: isActive, @@ -145,21 +149,26 @@ const toggleActive = (fietsenstalling_secties, fietstypeName, isActive): { return fietsenstalling_secties; } -const handleCapacityChange = (fietsenstalling_secties, fietstypeName, amount): { - fietsenstalling_secties: any, - fietstypeName: String, // 'fietstype' - amount: Number // 'Toegestaan' -} => { +const handleCapacityChange = (fietsenstalling_secties: ParkingSections, fietstypeName: any, amountstr: string): ParkingSections => { // It's mandatory to have at least 1 section - if(! fietsenstalling_secties) return fietsenstalling_secties; - if(! fietsenstalling_secties[0]) return fietsenstalling_secties; - if(! fietsenstalling_secties[0].secties_fietstype) return fietsenstalling_secties; + if (!fietsenstalling_secties) return fietsenstalling_secties; + if (!fietsenstalling_secties[0]) return fietsenstalling_secties; + if (!fietsenstalling_secties[0].secties_fietstype) return fietsenstalling_secties; + + // check for valid amount + let amount = 0 + try { + amount = parseInt(amountstr); + } catch (e) { + console.error("handleCapacityChange - amount is not a number"); + return fietsenstalling_secties; + } let didUpdateSomething = false; // Update the isActive/'Toegestaan' value for the 'fietstype' given fietsenstalling_secties[0].secties_fietstype = fietsenstalling_secties[0].secties_fietstype.map(x => { - if(x.fietstype.Name === fietstypeName) { + if (x.fietstype.Name === fietstypeName) { didUpdateSomething = true; } // Update 'Toegestaan' value if needed @@ -169,8 +178,8 @@ const handleCapacityChange = (fietsenstalling_secties, fietstypeName, amount): { Toegestaan: x.Toegestaan } }); - - if(! didUpdateSomething) { + + if (!didUpdateSomething) { // If above script didn't update a value, we should add this fietstypeName const newObject = { Toegestaan: false, @@ -193,12 +202,10 @@ const ParkingEditCapaciteit = ({ }: { parkingdata: ParkingDetailsType, capaciteitChanged: Function, - update: any + update: ParkingEditUpdateStructure }) => { // Variable to store the 'alle fietstypen' response - const [allFietstypen, setAllFietstypen ] = React.useState([]); - // Variable to store changed values in - const [updated, setUpdated] = React.useState([]); + const [allFietstypen, setAllFietstypen] = React.useState([]); // Set 'allFietstypen' local state React.useEffect(() => { @@ -206,16 +213,15 @@ const ParkingEditCapaciteit = ({ const result = await getAllFietstypen(); setAllFietstypen(result); })(); - },[]) + }, []) - let content = null; const capacitydata = calculateCapacityData(parkingdata); - // console.log("#### capacitydata", capacitydata, '#### parkingdata', parkingdata); - if (capacitydata===null || capacitydata?.unknown) { + let content = null; + if (capacitydata === null || capacitydata?.unknown) { content = "Onbekend"; - } else if (capacitydata.detailed===null || Object.keys(capacitydata.detailed).length === 0) { + } else if (capacitydata.detailed === null || Object.keys(capacitydata.detailed).length === 0) { content = ( <>
{parkingdata.Capacity}
@@ -252,6 +258,11 @@ const ParkingEditCapaciteit = ({ <>
{allFietstypen.map(x => { + if (x.Name === null) { + console.warn('Fietstype has no name', x); + return null; + } + const capacity = getCapacityForFietstype(x.Name, capacitydata, update.fietsenstalling_secties); const isAllowed = getAllowedValueForFietstype(x.Name, capacitydata, update.fietsenstalling_secties); return @@ -262,8 +273,9 @@ const ParkingEditCapaciteit = ({ { + console.log("**** PDFS", JSON.stringify(parkingdata.fietsenstalling_secties, null, 2)); const newFietsenstallingSecties = handleCapacityChange(parkingdata.fietsenstalling_secties, x.Name, e.target.value); const getNewCapacity = () => { let newCapacity = parkingdata; @@ -272,13 +284,16 @@ const ParkingEditCapaciteit = ({ }; capaciteitChanged(getNewCapacity().fietsenstalling_secties) }} - style={{width: '100px'}} + style={{ width: '100px' }} />
{ + onChange={(e) => { + if (null === parkingdata.fietsenstalling_secties) return; + if (null === x.Name) return; + const newFietsenstallingSecties = toggleActive(parkingdata.fietsenstalling_secties, x.Name, e.target.checked); const getNewCapacity = () => { let newCapacity = parkingdata; diff --git a/src/components/parking/ParkingView.tsx b/src/components/parking/ParkingView.tsx index fe02fca..c07c950 100644 --- a/src/components/parking/ParkingView.tsx +++ b/src/components/parking/ParkingView.tsx @@ -81,7 +81,7 @@ const ParkingView = ({ - -
- -
-
- - -
-

- Fietsenstalling Laag Catharijne -
- Catharijnesingel 34 -
- 3511 GB Utrecht -

-

- 0.3km -

-
- - - -
-
- Openingstijden -
-
-
Maandag
-
7:00 - 1:00
-
Dinsdag
-
7:00 - 1:00
- -
Woensdag
-
7:00 - 1:00
- -
Donderdag
-
7:00 - 1:00
-
Vrijdag
-
7:00 - 1:00
-
Zaterdag
-
7:00 - 1:00
-
Zondag
-
7:00 - 1:00
-
-
- - - -
-
- Tarief -
-
-
Fietsen
-
-
Eerste 24 uur:
-
gratis
-
Daarna per 24 uur:
-
€0,60
-
-
Bromfietsen
-
-
Eerste 24 uur:
-
€0,60
-
-
-
- - - -
-
- Services -
-
-
-
Buggy uitleen/verhuur
-
Fietspomp
-
Fietsverhuur
-
Reparatie
-
Toilet
-
-
-
- - - -
-
- Capaciteit -
-
-
-
Bromfietsen
-
32
-
Afwijkende maten
-
7
-
Elektrische fietsen
-
19
-
Bakfietsen
-
12
-
-
-
- - - -
-
- Abonnementen -
-
-
-
Jaarbonnement fiets
-
€80,90
-
Jaarabonnement bromfiets
-
€262.97
-
-
-
- - {/**/} - - - -
-
- Soort stalling -
-
Bewaakte stalling
-
- - - -
-
- Beheerder -
-
U-stal
-
- - {/**/} -
- -
- Kaart -
-
- -
- ); - }; - - const renderCrud = () => { - return null; - }; - - return editmode ? renderCrud() : renderReadonly(); -}; - -export default Stalling; diff --git a/src/pages/stalling.tsx b/src/pages/stalling.tsx deleted file mode 100644 index f932d1b..0000000 --- a/src/pages/stalling.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -import { getServerSession } from "next-auth/next" -import { authOptions } from '~/pages/api/auth/[...nextauth]' - -// Import utils -import { getParkingsFromDatabase } from "~/utils/prisma"; - -// Import components -import AppHeaderDesktop from "~/components/AppHeaderDesktop"; -import PageTitle from "~/components/PageTitle"; -import ContentPageWrapper from "~/components/ContentPageWrapper"; -import ImageSlider from "~/components/ImageSlider"; -import HorizontalDivider from "~/components/HorizontalDivider"; -import CloseButton from "~/components/CloseButton"; -import { Button } from "~/components/Button"; -import Parking from "~/components/Parking"; - -export async function getServerSideProps(context) { - try { - const session = await getServerSession(context.req, context.res, authOptions) - const sites = session?.user?.sites || []; - const fietsenstallingen = await getParkingsFromDatabase(sites); - - return { - props: { - fietsenstallingen: fietsenstallingen, - online: true, - }, - }; - } catch (ex: any) { - // console.error("index.getStaticProps - error: ", ex.message); - return { - props: { - fietsenstallingen: [], - online: false, - }, - }; - } -} - -const Stalling: NextPage = ({ fietsenstallingen, online }: any) => { - return ( -
- -
- Utrecht Laag Catharijne - - -
-
- - { - return stalling.ID === "E2C31818-9B25-5299-71B170A5B41BA07F"; - })} /> - -
- ); -}; - -export default Stalling; diff --git a/src/store/filterSlice.ts b/src/store/filterSlice.ts index ac2a183..370f7d3 100644 --- a/src/store/filterSlice.ts +++ b/src/store/filterSlice.ts @@ -6,6 +6,7 @@ import { HYDRATE } from "next-redux-wrapper"; export interface FilterState { activeTypes: String[]; query: String; + activeTypes2: String[]; // used to store the showSubmissions filter } // Initial state @@ -16,6 +17,7 @@ const initialState: FilterState = { "toezicht" ], query: "", + activeTypes2: [] }; const allowedTypes = [ @@ -28,6 +30,10 @@ const allowedTypes = [ 'fietskluizen' ]; +const allowedTypes2 = [ + 'show_submissions', +]; + // Actual Slice export const filterSlice = createSlice({ name: "filter", @@ -50,21 +56,52 @@ export const filterSlice = createSlice({ const typesToSet = action.payload; // Check if given types are valid let isInvalidInput = false; - if(! typesToSet) { + if (!typesToSet) { isInvalidInput = true; } - for(let key in typesToSet) { + for (let key in typesToSet) { const typeToSet = typesToSet[key]; - if(allowedTypes.indexOf(typeToSet) <= -1) { + if (allowedTypes.indexOf(typeToSet) <= -1) { isInvalidInput = true; } } - if(isInvalidInput) { + if (isInvalidInput) { return; } state.activeTypes = typesToSet; }, + toggleType2(state, action) { + const pfType = action.payload.pfType; + const index = state.activeTypes2.indexOf(pfType); + if (index === -1) { + // Add the type to the array if it's not present + state.activeTypes2.push(pfType); + } else { + // Remove the type from the array if it's present + state.activeTypes2.splice(index, 1); + } + }, + // Action to toggle the type + setTypes2(state, action) { + const typesToSet = action.payload; + // Check if given types are valid + let isInvalidInput = false; + if (!typesToSet) { + isInvalidInput = true; + } + for (let key in typesToSet) { + const typeToSet = typesToSet[key]; + if (allowedTypes2.indexOf(typeToSet) <= -1) { + isInvalidInput = true; + } + } + if (isInvalidInput) { + return; + } + + state.activeTypes2 = typesToSet; + }, setQuery(state, action) { state.query = action.payload; }, @@ -83,6 +120,8 @@ export const filterSlice = createSlice({ export const { toggleType, - setQuery, - setTypes + toggleType2, + setTypes, + setTypes2, + setQuery } = filterSlice.actions; diff --git a/src/types/index.ts b/src/types/index.ts index f6f6152..c08f63a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,8 +1,23 @@ /* This type is used when returning parking details to the client */ /* By adding fields to this structure, it is possible to keep track which fields */ -/* from the "old" database are in use */ +/* from the "old" database are in use */ export type DayPrefix = 'ma' | 'di' | 'wo' | 'do' | 'vr' | 'za' | 'zo'; +export type ParkingSectionPerBikeType = { + Toegestaan: boolean | null, + Capaciteit: number | null, + fietstype: { + Name: string // Assuming Name is of type string + } +} + +export type ParkingSection = { + titel: string, + secties_fietstype: ParkingSectionPerBikeType[] // base data for capacity +} + +export type ParkingSections = ParkingSection[]; + export type ParkingDetailsType = { ID: string, Title: string, @@ -11,19 +26,19 @@ export type ParkingDetailsType = { Plaats: string, Type: string, Image: any; - Open_ma : Date, + Open_ma: Date, Dicht_ma: Date, - Open_di : Date, + Open_di: Date, Dicht_di: Date, - Open_wo : Date, + Open_wo: Date, Dicht_wo: Date, - Open_do : Date, + Open_do: Date, Dicht_do: Date, - Open_vr : Date, + Open_vr: Date, Dicht_vr: Date, - Open_za : Date, + Open_za: Date, Dicht_za: Date, - Open_zo : Date, + Open_zo: Date, Dicht_zo: Date, Openingstijden: string, Capacity: number, @@ -36,17 +51,7 @@ export type ParkingDetailsType = { name: string, sequence: number, }[], - fietsenstalling_secties: { - titel: string, - secties_fietstype: // base data for capacity - { - Toegestaan: boolean | null, - Capaciteit: number | null, - fietstype: { - Name: string // Assuming Name is of type string - } - }[] - }[], + fietsenstalling_secties: ParkingSections, abonnementsvorm_fietsenstalling: { abonnementsvormen: { ID: string, @@ -70,11 +75,12 @@ export type ParkingDetailsType = { Helpdesk: string, CompanyName: string, }, - fietsenstallingen_services: { - services: { - ID: string, - Name: string + fietsenstallingen_services: [ + { + services: { + ID: string, + Name: string + } } - - } - } \ No newline at end of file + ] +} \ No newline at end of file diff --git a/src/utils/auth-tools.ts b/src/utils/auth-tools.ts index 44915ce..185f716 100644 --- a/src/utils/auth-tools.ts +++ b/src/utils/auth-tools.ts @@ -4,15 +4,18 @@ // import type { User } from "next-auth"; import bcrypt from "bcrypt"; import { prisma } from "~/server/db"; +import { DefaultUser } from "next-auth"; -export type Account = { - email: string; - password_hash: string; - }; +export interface User extends DefaultUser { + OrgUserID: string | null, + OtherUserID: string | null, + org_account_type: number | null, + sites: string[], +} export const getUserFromCredentials = async ( credentials: Record<"email" | "password", string> | undefined -) => { +): Promise => { if (!credentials) return null; console.log("### getUserFromCredentials", credentials); @@ -21,21 +24,28 @@ export const getUserFromCredentials = async ( if (!email || !password) return null; let validaccount = false; - let account = { + let account: User = { + id: "", email: email.toLocaleLowerCase(), OrgUserID: null, OtherUserID: null, org_account_type: null, + sites: [], }; // check if this is an organizational account via security_accounts table const orgaccount = await prisma.security_users.findFirst({ where: { UserName: email.toLowerCase() } }); - if(orgaccount!==undefined && orgaccount!==null && orgaccount.EncryptedPassword!==null) { - if(bcrypt.compareSync(password, orgaccount.EncryptedPassword)||true) { + if (orgaccount !== undefined && orgaccount !== null && orgaccount.EncryptedPassword !== null) { + console.log("got orgaccount", orgaccount); + if (bcrypt.compareSync(password, orgaccount.EncryptedPassword)) { validaccount = true; + account.id = orgaccount.UserID; account.OrgUserID = orgaccount.UserID; account.org_account_type = orgaccount.RoleID; + const sites = await prisma.security_users_sites.findMany({ where: { UserID: orgaccount.UserID } }); + console.log("got sites", sites); + // console.log("### getUserFromCredentials - found account in security_users table -", account); } else { console.log("### getUserFromCredentials - invalid password for security_users table"); diff --git a/src/utils/parkings.tsx b/src/utils/parkings.tsx index edd2b36..33ffda3 100644 --- a/src/utils/parkings.tsx +++ b/src/utils/parkings.tsx @@ -1,7 +1,8 @@ import React from "react"; +import { Session } from "next-auth"; + import { - PrismaClient, type fietsenstallingen, } from "@prisma/client"; import type { ParkingDetailsType, DayPrefix } from "~/types/"; @@ -29,7 +30,7 @@ export const getParkingDetails = async (stallingId: string): Promise { +export const getAllServices = async (): Promise => { try { const response = await fetch( `/api/services/` @@ -43,7 +44,7 @@ export const getAllServices = async (): Promise => { } }; -export const getAllFietstypen = async (): Promise => { +export const getAllFietstypen = async (): Promise => { try { const response = await fetch( `/api/fietstypen` @@ -117,7 +118,7 @@ export const generateRandomId = (prefix = '') => { } if (prefix.length > 8) { - prefix = prefix.substr(0, 8); + prefix = prefix.substring(0, 8); } let id = `${prefix}-`; @@ -131,4 +132,152 @@ export const generateRandomId = (prefix = '') => { return id; } + +export const newStallingIDForThisUser = (session?: Session): string | false => { + if (session?.user?.OrgUserID) { + return 'NW' + session.user.OrgUserID.substring(2); + } else { + return false; + } +} + +export const isNewStallingID = (stallingID: string): boolean => { + return stallingID.substring(0, 2) === 'NW' || stallingID.substring(0, 8) === 'VOORSTEL'; +} + +export const getNewStallingDefaultRecord = (ID: string) => { + const data: ParkingDetailsType = { + ID, + Title: 'Nieuwe stalling', + Location: "", + Postcode: "", + Plaats: "", + Type: "bewaakt", + Image: null, + Open_ma: new Date(0), + Dicht_ma: new Date(0), + Open_di: new Date(0), + Dicht_di: new Date(0), + Open_wo: new Date(0), + Dicht_wo: new Date(0), + Open_do: new Date(0), + Dicht_do: new Date(0), + Open_vr: new Date(0), + Dicht_vr: new Date(0), + Open_za: new Date(0), + Dicht_za: new Date(0), + Open_zo: new Date(0), + Dicht_zo: new Date(0), + Openingstijden: "", + Capacity: 0, + Coordinaten: '52.09066,5.121317', + FMS: false, + Beheerder: "", + BeheerderContact: "", + } + + return data +} + +export const getNewStallingRecord = async (session?: Session): Promise => { + try { + if (!session) { + // when no user is logged in, a default stalling record is created + console.log("NO USER LOGGED IN") + return getNewStallingDefaultRecord(generateRandomId('VOORSTEL')); + } else { + const voorstelid = generateRandomId(); + // for logged in users, the stalling record is fetched from the database + const response: Response = await fetch( + "/api/fietsenstallingen?id=" + voorstelid, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + if (response.status !== 200) { + console.log("new stalling does not exist for this user"); + return false; // new stalling does not exist for this user + } + + let data = await response.json(); + console.log("getNewStallingRecord: got newStallingData", data); + return data + } + } catch (ex) { + console.error('getNewStallingForUser', ex); + return false; + } +} + +// export const removeNewStallingForUser = async (session: Session): Promise => { +// try { +// const data = await getNewStallingRecord(session); +// if (false === data) { +// console.warn('removeNewStallingForUser: no new stalling record found for this user'); +// return false; +// } + +// console.log('removeNewStallingForUser: remove record with ID', data.ID); +// const response = await fetch( +// "/api/fietsenstallingen?id=" + data.ID, +// { +// method: "DELETE", +// headers: { +// "Content-Type": "application/json", +// }, +// }); +// console.log("delete response", response); +// return response.status === 200; +// } catch (ex) { +// console.error('removeNewStallingForUser: error ', ex); +// return false; +// } +// } + +// export const finalizeNewStallingForUser = async (session: Session): Promise => { +// const data = await getNewStallingRecord(session); +// if (false === data) { +// console.warn('finalizeNewStallingForUser: no new stalling record found for this user'); +// return false; +// } + +// console.log('got newstalling data record', data); + +// const tempID = data.ID; +// data.ID = generateRandomId(); + +// let response = await fetch( +// "/api/fietsenstallingen", +// { +// method: "POST", +// body: JSON.stringify(data), +// headers: { +// "Content-Type": "application/json", +// }, +// }); +// if (response.status === 201) { +// const response = await fetch( +// "/api/fietsenstallingen?id=" + tempID, +// { +// method: "DELETE", +// headers: { +// "Content-Type": "application/json", +// }, +// }); +// if (response.status === 200) { +// return true; +// } else { +// alert(`Er is iets misgegaan bij het opslaan van de nieuwe stalling [code 1-${response.status}]. Probeer het later nog eens.`); +// return false; +// } +// } else { +// alert(`Er is iets misgegaan bij het opslaan van de nieuwe stalling [code 2-${response.status}]. Probeer het later nog eens.`); +// return false; +// } +// } + + export default generateRandomId; \ No newline at end of file diff --git a/src/utils/prisma.ts b/src/utils/prisma.ts index 2ee72eb..e852f5f 100644 --- a/src/utils/prisma.ts +++ b/src/utils/prisma.ts @@ -1,35 +1,32 @@ import { Prisma } from "@prisma/client"; import { prisma } from "~/server/db"; -const getParkingsFromDatabase = async (sites:any) => { +const getParkingsFromDatabase = async (sites: any) => { let fietsenstallingen; - if(sites.length===0) { + if (sites.length === 0) { fietsenstallingen = await prisma.fietsenstallingen.findMany({ where: { Status: "1", - // Plaats: { - // not: "", - // } }, - // select: { - // StallingsID: true, - // Title: true, - // Location: true, - // Coordinaten: true, - // DateCreated: true, - // }, }); } else { fietsenstallingen = await prisma.fietsenstallingen.findMany({ where: { - Status: "1", - // Plaats: { - // not: "", - // }, - SiteID: { in: sites }, - }, + OR: [{ + Status: "1", + // Plaats: { + // not: "", + // }, + SiteID: { in: sites }, + }, + { + ID: { + startsWith: 'VOORSTEL' + } + }], + } }); } diff --git a/tsconfig.json b/tsconfig.json index fbc509a..2861707 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,12 @@ { "compilerOptions": { "target": "es2017", - "lib": ["dom", "dom.iterable", "esnext", "es6"], + "lib": [ + "dom", + "dom.iterable", + "esnext", + "es6" + ], "allowJs": true, "checkJs": true, "skipLibCheck": true, @@ -18,7 +23,9 @@ "noUncheckedIndexedAccess": true, "baseUrl": ".", "paths": { - "~/*": ["./src/*"] + "~/*": [ + "./src/*" + ] } }, "include": [ @@ -29,5 +36,7 @@ "**/*.cjs", "**/*.mjs" ], - "exclude": ["node_modules"] -} + "exclude": [ + "node_modules" + ] +} \ No newline at end of file From 49ad6561f57f86f7a65727d831de2880b0c558ce Mon Sep 17 00:00:00 2001 From: bartwr Date: Thu, 21 Mar 2024 17:27:26 +0100 Subject: [PATCH 03/15] Add Account table for 'send magic login link' --- prisma/schema.prisma | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ffef650..aa3180f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2025,3 +2025,48 @@ enum accounts_account_type { // sync // reservation // } + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} From 043940efafaecce3a5e51cfa8c57628bc7cc5151 Mon Sep 17 00:00:00 2001 From: Marc Buma Date: Mon, 25 Mar 2024 16:08:08 +0100 Subject: [PATCH 04/15] Items Besproken op 2024-03-11 --- github/setup-actions.sh | 2 +- package-lock.json | 24 ++ package.json | 1 + src/components/Form/FormInput.tsx | 9 +- src/components/MapComponent.tsx | 16 +- src/components/Parking.tsx | 11 +- src/components/ParkingFacilityBlock.tsx | 23 +- src/components/ParkingOnTheMap.tsx | 4 + src/components/parking/ParkingEdit.tsx | 373 +++++++++++++++--- .../parking/ParkingEditLocation.tsx | 4 + src/components/parking/ParkingViewOpening.tsx | 20 +- src/pages/_app.tsx | 34 +- src/pages/api/parking/[ID].ts | 11 +- src/pages/api/services/index.ts | 11 +- .../subscription_types_for_parking/index.ts | 7 +- src/pages/index.tsx | 39 +- src/store/mapSlice.ts | 17 +- src/types/index.ts | 2 + src/utils/map/active_municipality.ts | 27 +- src/utils/municipality.ts | 22 +- src/utils/navigation.ts | 42 +- src/utils/nomatim.ts | 88 +++++ src/utils/parkings.tsx | 31 +- 23 files changed, 645 insertions(+), 173 deletions(-) create mode 100644 src/utils/nomatim.ts diff --git a/github/setup-actions.sh b/github/setup-actions.sh index ce9fab6..7b71246 100755 --- a/github/setup-actions.sh +++ b/github/setup-actions.sh @@ -10,6 +10,6 @@ gh variable set PROD_AZUREAPPSERVICE_PUBLISHPROFILE_VEILIGSTALLEN < ./github/pro gh variable set ACC_AZUREAPPSERVICE_PUBLISHPROFILE_VEILIGSTALLEN < ./github/acceptance.PublishSettings #enable the ci/cd action -gh workflow enable azure-webapps-node.yml +#gh workflow enable azure-webapps-node.yml gh variable list diff --git a/package-lock.json b/package-lock.json index a074789..dbf73c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "next-redux-wrapper": "^8.1.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-hot-toast": "^2.4.1", "react-icons": "^4.8.0", "react-redux": "^8.0.5", "superjson": "1.12.2", @@ -8341,6 +8342,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -11240,6 +11249,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", diff --git a/package.json b/package.json index e3767ba..4babf00 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "next-redux-wrapper": "^8.1.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-hot-toast": "^2.4.1", "react-icons": "^4.8.0", "react-redux": "^8.0.5", "superjson": "1.12.2", diff --git a/src/components/Form/FormInput.tsx b/src/components/Form/FormInput.tsx index 1ba2ab3..1013bd8 100644 --- a/src/components/Form/FormInput.tsx +++ b/src/components/Form/FormInput.tsx @@ -12,7 +12,8 @@ function FormInput({ label, size, style, - defaultValue + defaultValue, + disabled }: { innerRef?: React.LegacyRef, type?: string, @@ -25,6 +26,7 @@ function FormInput({ label?: string, size?: number style?: object + disabled?: boolean }) { return ( <> @@ -32,8 +34,8 @@ function FormInput({ {label ?
{label}
: ''} - diff --git a/src/components/MapComponent.tsx b/src/components/MapComponent.tsx index dcdafae..7ceaeec 100644 --- a/src/components/MapComponent.tsx +++ b/src/components/MapComponent.tsx @@ -4,6 +4,7 @@ import maplibregl from "maplibre-gl"; import * as turf from "@turf/turf"; import { useDispatch, useSelector } from "react-redux"; import { + setMapCurrentLatLong, setMapExtent, setMapZoom, setMapVisibleFeatures, @@ -180,6 +181,10 @@ function MapboxMap({ fietsenstallingen = [] }: any) { zoom: 7, }); + mapboxMap.on('styleimagemissing', (e) => { + mapboxMap.addImage(e.id, { width: 0, height: 0, data: new Uint8Array(0) }); + }); + mapboxMap.on("load", () => onMapLoaded(mapboxMap)); // Function that executes if component unloads: @@ -208,9 +213,11 @@ function MapboxMap({ fietsenstallingen = [] }: any) { if ( !stateMap || stateMap === undefined || - "getSource" in stateMap === false + "getSource" in stateMap === false || + "getLayer" in stateMap === false ) return; + if (!stateMap.getSource) return; // Create geojson @@ -218,6 +225,9 @@ function MapboxMap({ fietsenstallingen = [] }: any) { // Add or update fietsenstallingen data as sources const addOrUpdateSource = (sourceKey) => { + if (!stateMap || stateMap === undefined) { + return; + } const source: maplibregl.GeoJSONSource = stateMap.getSource( sourceKey ) as maplibregl.GeoJSONSource; @@ -336,7 +346,7 @@ function MapboxMap({ fietsenstallingen = [] }: any) { // Filter map markers if filterActiveTypes filter changes React.useEffect(() => { try { - if (!stateMap) return; + if (!stateMap || stateMap === undefined) return; let filter = [ "all", @@ -493,6 +503,8 @@ function MapboxMap({ fietsenstallingen = [] }: any) { })(); // Set values in state + const coordinates = [center.geometry.coordinates[0].toString(), center.geometry.coordinates[1].toString()] + dispatch(setMapCurrentLatLong(coordinates)); dispatch(setMapExtent(extent)); dispatch(setMapZoom(theMap.getZoom())); }, []); diff --git a/src/components/Parking.tsx b/src/components/Parking.tsx index 8c5cc4d..8596a12 100644 --- a/src/components/Parking.tsx +++ b/src/components/Parking.tsx @@ -1,6 +1,8 @@ import React, { useState, useEffect } from "react"; +import { useSelector } from "react-redux"; import { useSession } from "next-auth/react"; import { useRouter } from 'next/router' +import { AppState } from "~/store/store"; import { type ParkingDetailsType } from "~/types/"; import { getParkingDetails, getNewStallingDefaultRecord } from "~/utils/parkings"; @@ -16,6 +18,10 @@ const Parking = () => { const [currentStalling, setCurrentStalling] = useState(null); const [editMode, setEditMode] = React.useState(false); + const currentLatLong = useSelector( + (state: AppState) => state.map.currentLatLng + ); + useEffect(() => { if (router.query.stallingid === undefined || Array.isArray(router.query.stallingid)) { return; @@ -35,7 +41,7 @@ const Parking = () => { // when no user is logged in, a recognizalbe prefix is used prefix = 'VOORSTEL'; } - setCurrentStalling(getNewStallingDefaultRecord("")); + setCurrentStalling(getNewStallingDefaultRecord("", currentLatLong)); setEditMode(true); } else { getParkingDetails(stallingId).then((stalling) => { @@ -69,11 +75,12 @@ const Parking = () => { setCurrentRevision(currentRevision + 1); } - const allowEdit = session.status === "authenticated" || currentStalling && currentStalling.ID === ""; if (null === currentStalling) { return null; } + let allowEdit = session.status === "authenticated" || currentStalling && currentStalling.ID === ""; + if (allowEdit === true && (editMode === true)) { return (); } else { diff --git a/src/components/ParkingFacilityBlock.tsx b/src/components/ParkingFacilityBlock.tsx index c146661..6701b0d 100644 --- a/src/components/ParkingFacilityBlock.tsx +++ b/src/components/ParkingFacilityBlock.tsx @@ -30,19 +30,21 @@ type ParkingType = { Dicht_za?: string; Open_zo?: string; Dicht_zo?: string; + Image?: string; + ExtraServices?: string; } -const isOpen = (openingTime: Date, closingTime: Date): boolean => { +const isOpen = (openingTime: Date, closingTime: Date, isNS: boolean = false): boolean => { const now = new Date(); const currentTime = now.getHours() * 60 + now.getMinutes(); const opening = openingTime.getHours() * 60 + openingTime.getMinutes(); let closing = closingTime.getHours() * 60 + closingTime.getMinutes(); - // Exception for NS parkings: If NS parking AND open from 1am to 1am, - // then the parking is open 24 hours per day. // #TODO: Combine functions with /src/utils/parkings.tsx if (opening === closing && opening === 60 && closing === 60) { - return true; + // Exception for NS parkings: If NS parking AND open from 1am to 1am, + // then the parking is open 24 hours per day. + return isNS; } if (closing < opening) { @@ -73,7 +75,8 @@ const formatOpeningToday = (parkingdata: any): string => { const openinfo = new Date(openstr); const closeinfo = new Date(closestr); - if (isOpen(openinfo, closeinfo)) { + const isNS = parkingdata.EditorCreated === "NS-connector"; + if (isOpen(openinfo, closeinfo, isNS)) { let str = `open`; // Exception: If this is a 24/h a day @@ -100,7 +103,9 @@ function ParkingFacilityBlock({ }: { id?: any, parking: ParkingType, + compact: boolean openParkingHandler?: Function, + expandParkingHandler?: Function, showButtons?: false }) { const { push } = useRouter(); @@ -136,8 +141,8 @@ function ParkingFacilityBlock({ const openingDescription = formatOpeningToday(parking); - const detailsLine = `${costDescription}${costDescription && openingDescription ? "| " : "" - }${openingDescription}`; + // const detailsLine = `${costDescription}${costDescription && openingDescription ? "| " : "" + // }${openingDescription}`; if (parking.ExtraServices) { // console.log('parking', parking) @@ -174,7 +179,7 @@ function ParkingFacilityBlock({ } `} style={{ - backgroundColor: !compact ? 'rgba(31, 153, 210, 0.1)' : null + backgroundColor: !compact ? 'rgba(31, 153, 210, 0.1)' : undefined }} onClick={() => { // Expand parking if expandParkingHandler was given @@ -182,7 +187,7 @@ function ParkingFacilityBlock({ expandParkingHandler(parking.ID); } // Open parking details if ParkingBlock was already active - else if (expandParkingHandler && !compact) { + else if (expandParkingHandler && openParkingHandler && !compact) { openParkingHandler(parking.ID); } // Open parking if no expand handler was given diff --git a/src/components/ParkingOnTheMap.tsx b/src/components/ParkingOnTheMap.tsx index 74ba55e..48462fd 100644 --- a/src/components/ParkingOnTheMap.tsx +++ b/src/components/ParkingOnTheMap.tsx @@ -97,6 +97,10 @@ function ParkingOnTheMap({ parking }) { }); mapboxMap.on("load", () => onMapLoaded(mapboxMap)); + mapboxMap.on('styleimagemissing', (e) => { + mapboxMap.addImage(e.id, { width: 0, height: 0, data: new Uint8Array(0) }); + }); + // Function that executes if component unloads: return () => { diff --git a/src/components/parking/ParkingEdit.tsx b/src/components/parking/ParkingEdit.tsx index 129aed1..77d2b6d 100644 --- a/src/components/parking/ParkingEdit.tsx +++ b/src/components/parking/ParkingEdit.tsx @@ -5,13 +5,16 @@ import PageTitle from "~/components/PageTitle"; import HorizontalDivider from "~/components/HorizontalDivider"; import { Button, IconButton } from "~/components/Button"; import FormInput from "~/components/Form/FormInput"; +import FormCheckbox from "~/components/Form/FormCheckbox"; import SectionBlock from "~/components/SectionBlock"; import SectionBlockEdit from "~/components/SectionBlockEdit"; import type { ParkingDetailsType, ParkingSections } from "~/types/"; import { getAllServices, generateRandomId, + getDefaultLocation, } from "~/utils/parkings"; +import { cbsCodeFromMunicipality } from "~/utils/municipality"; import { Tabs, Tab, FormHelperText, FormLabel, Typography } from "@mui/material"; /* Use nicely formatted items for items that can not be changed yet */ @@ -22,6 +25,10 @@ import ParkingEditLocation from "~/components/parking/ParkingEditLocation"; import ParkingEditAfbeelding from "~/components/parking/ParkingEditAfbeelding"; import ParkingEditOpening, { type OpeningChangedType } from "~/components/parking/ParkingEditOpening"; import { useSession } from "next-auth/react" +import ParkingViewBeheerder from "./ParkingViewBeheerder"; +import { type MunicipalityType, getMunicipalityBasedOnLatLng } from "~/utils/map/active_municipality"; +import { geocodeAddress, reverseGeocode } from "~/utils/nomatim"; +import toast from 'react-hot-toast'; export type ParkingEditUpdateStructure = { ID?: string; @@ -31,6 +38,7 @@ export type ParkingEditUpdateStructure = { Plaats?: string; Coordinaten?: string; Type?: string; + SiteID?: string; // [key: string]: string | undefined; Openingstijden?: any; // Replace with the actual type if different fietsenstalling_secties?: ParkingSections; // Replace with the actual type if different @@ -44,6 +52,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD const [selectedTab, setSelectedTab] = React.useState('tab-algemeen'); // const [selectedTab, setSelectedTab] = React.useState('tab-capaciteit'); + const [newSiteID, setNewSiteID] = React.useState(undefined); const [newTitle, setNewTitle] = React.useState(undefined); const [newLocation, setNewLocation] = React.useState(undefined); const [newPostcode, setNewPostcode] = React.useState(undefined); @@ -53,6 +62,10 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD // used for map recenter when coordinates are manually changed const [centerCoords, setCenterCoords] = React.useState(undefined); + // beheerder + const [newBeheerder, setNewBeheerder] = React.useState(undefined); + const [newBeheerderContact, setNewBeheerderContact] = React.useState(undefined); + // type FietsenstallingSectiesType = { [key: string]: Array[] } const [allServices, setAllServices] = React.useState([]); @@ -66,6 +79,8 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD const [allTypes, setAllTypes] = React.useState([]); const [newStallingType, setNewStallingType] = React.useState(undefined); + const [currentMunicipality, setCurrentMunicipality] = React.useState(undefined); + const { data: session } = useSession() // Set 'allServices' variable in local state @@ -92,10 +107,98 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD })(); }, []) + React.useEffect(() => { + updateSiteID(); + }, [parkingdata.Location, newCoordinaten]); + const handleChange = (event: React.SyntheticEvent, newValue: string) => { setSelectedTab(newValue); } + type checkInfo = { + type: "string" | "coordinaten", + text: string, + value: any, + newvalue: any + } + + const updateSiteID = () => { + const currentll = undefined !== newCoordinaten ? newCoordinaten : parkingdata.Coordinaten; + getMunicipalityBasedOnLatLng(currentll.split(",")).then((result) => { + if (result !== false) { + setCurrentMunicipality(result); + const cbsCode = cbsCodeFromMunicipality(result); + if (cbsCode.toString() !== parkingdata.SiteID) { + setNewSiteID(cbsCode ? cbsCode.toString() : undefined); + } else { + setNewSiteID(undefined); + } + } else { + setCurrentMunicipality(undefined); + setNewSiteID(undefined); + } + }).catch((err) => { + console.error('municipality based on latlng error', err); + }) + } + + const validateParkingData = (): boolean => { + const checkStringType = (check: checkInfo): string => { + if ((check.value === "" || check.value === null) && check.newvalue === undefined) { + return `invoer van ${check.text} is verplicht`; + } else { + return ""; + } + } + + const checkCoordinatenType = (check: checkInfo): string => { + if (check.value === getDefaultLocation && check.newvalue === undefined) { + return `${check.text} is verplicht`; + } else { + return ""; + } + } + + let checks: checkInfo[] = [ + { type: "string", text: "invoer van de titel", value: parkingdata.Title, newvalue: newTitle }, + { type: "string", text: "invoer van de straat en huisnummer", value: parkingdata.Location, newvalue: newLocation }, + { type: "string", text: "invoer van de postcode", value: parkingdata.Postcode, newvalue: newPostcode }, + { type: "string", text: "invoer van de plaats", value: parkingdata.Plaats, newvalue: newPlaats }, + { type: "string", text: "selectie van de gemeente", value: parkingdata.SiteID, newvalue: newSiteID }, + { type: "coordinaten", text: "instellen van de locatie op de kaart", value: parkingdata.Coordinaten, newvalue: newCoordinaten }, + ] + + // FMS & ExploitantID cannot be changed for now, so no need to check those for changes + if (parkingdata.FMS !== true && parkingdata.ExploitantID === null) { + checks.push({ type: "string", text: "invoer van de contactgegevens van de beheerder", value: parkingdata.Beheerder, newvalue: newBeheerder }); + checks.push({ type: "string", text: "invoer van de contactgegevens van de beheerder", value: parkingdata.BeheerderContact, newvalue: newBeheerderContact }); + } + + // Not checked / check not required + // Type, Image, Openingstijden, Capacity, FMS, Beheerder, BeheerderContact, fietsenstalling_type, fietsenstalling_secties, abonnementsvorm_fietsenstalling, exploitant, fietsenstallingen_services + + let message = ""; + for (const check of checks) { + switch (check.type) { + case "string": + message = checkStringType(check); + break; + case "coordinaten": + message = checkCoordinatenType(check); + break; + default: + break; + } + + if (message !== "") { + alert(message); + return false; + } + }; + + return true; + } + const getUpdate = () => { let update: ParkingEditUpdateStructure = {}; @@ -105,6 +208,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD if (newPlaats !== undefined) { update.Plaats = newPlaats; } if (newCoordinaten !== undefined) { update.Coordinaten = newCoordinaten; } if (newStallingType !== undefined) { update.Type = newStallingType; } + if (newSiteID !== undefined) { update.SiteID = newSiteID; } if (undefined !== newOpening) { for (const keystr in newOpening) { @@ -202,11 +306,87 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD } const parkingChanged = () => { - console.log("PARKINGCHANGED", newServices.length > 0, newCapaciteit.length > 0, newOpening !== undefined, newOpeningstijden !== undefined); + // console.log("PARKINGCHANGED", newServices.length > 0, newCapaciteit.length > 0, newOpening !== undefined, newOpeningstijden !== undefined); return Object.keys(update).length !== 0 || newServices.length > 0 || newCapaciteit.length > 0 || newOpening !== undefined || newOpeningstijden !== undefined; } + const acceptParking = async (): Promise => { + try { + const newID = generateRandomId(); + + // create a new parking record with the given ID + console.log(">>>> move parking record from ", parkingdata.ID, " to ", newID); + // console.log("store changes", isNew, JSON.stringify(update)); + + const storedata = Object.assign({}, parkingdata, { ID: newID }); + storedata.Open_ma = new Date(parkingdata.Open_ma); // Datetime + storedata.Dicht_ma = new Date(parkingdata.Dicht_ma); // Datetime + storedata.Open_di = new Date(parkingdata.Open_di); // Datetime + storedata.Dicht_di = new Date(parkingdata.Dicht_di); // Datetime + storedata.Open_wo = new Date(parkingdata.Open_wo); // Datetime + storedata.Dicht_wo = new Date(parkingdata.Dicht_wo); // Datetime + storedata.Open_do = new Date(parkingdata.Open_do); // Datetime + storedata.Dicht_do = new Date(parkingdata.Dicht_do); // Datetime + storedata.Open_vr = new Date(parkingdata.Open_vr); // Datetime + storedata.Dicht_vr = new Date(parkingdata.Dicht_vr); // Datetime + storedata.Open_za = new Date(parkingdata.Open_za); // Datetime + storedata.Dicht_za = new Date(parkingdata.Dicht_za); // Datetime + storedata.Open_zo = new Date(parkingdata.Open_zo); // Datetime + storedata.Dicht_zo = new Date(parkingdata.Dicht_zo); // Datetime + + const result = await fetch( + "/api/fietsenstallingen?id=" + newID, + { + method: "POST", + body: JSON.stringify(storedata), + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!result.ok) { + toast("Accpeteren is mislukt. Probeer het later nog eens.") + + await fetch( + "/api/fietsenstallingen?id=" + newID, + { method: "DELETE" } + ); + throw Error('Er ging iets fout bij het verplaatsen') + } else { + await fetch( + "/api/fietsenstallingen?id=" + parkingdata.ID, + { method: "DELETE" } + ); + + toast("De stalling is geaccepteerd.") + + return true; + } + + // store parking record + + // store fietsenstallingen_services records + + // store fietsenstalling_secties records + + // store abonnementsvorm_fietsenstalling records + + // delete old fietsenstallingen_services records + + // delete old fietsenstalling_secties records + + // delete old abonnementsvorm_fietsenstalling records + + // delete old parking record + return false; + } catch (ex: unknown) { + console.error("ParkingEdit - unable to move parking record to new ID"); + return false; + } + } + const handleUpdateParking = async () => { // Stop if no parking ID is available if (!parkingdata) return; @@ -216,6 +396,13 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD parkingdata.ID = generateRandomId(session ? "" : "VOORSTEL"); } + const isVoorstel = parkingdata?.ID.substring(0, 8) === 'VOORSTEL' + + if (!validateParkingData()) { + console.warn("ParkingEdit - invalid data: update cancelled"); + return; + } + // Check if parking was changed const update = getUpdate(); @@ -254,10 +441,27 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD ); if (!result.ok) { throw Error('Er ging iets fout bij het opslaan. Controleer of de gegevens kloppen. Is de postcode bijvoorbeeld juist, en niet te lang?') + } else { + if (isVoorstel === false) { + toast("De stalling is opgeslagen.", { duration: 5000 }) + } else { + toast(`Uw voorstel wordt aangemeld bij gemeente ${currentMunicipality?.name}.`, { duration: 30000, style: { minWidth: '40vw' } }) + } } // Go back to 'view' mode onChange(); - onClose(isNew ? parkingdata.ID : undefined); + + let newID = undefined; + if (isNew) { + newID = parkingdata.ID + } else if (isVoorstel) { + newID = await acceptParking(); + if (false === newID) { + console.log("unable to accept parking"); + newID = undefined; // unable to update ID + }; + } + onClose(newID); } catch (err: any) { if (err.message) alert(err.message); else alert(err); @@ -316,7 +520,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD } } - const renderTabAlgemeen = () => { + const renderTabAlgemeen = (visible: boolean = false) => { const serviceIsActive = (ID: string): boolean => { const change = newServices.find(s => (s.ID === ID)); if (change !== undefined) { @@ -351,8 +555,39 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD setNewServices([...newServices]); } + const addressValid = () => { + return ((parkingdata.Location !== "" || newLocation !== undefined) && (parkingdata.Plaats !== "" || newPlaats !== undefined)) || + (parkingdata.Postcode !== "" || newPostcode !== undefined) + } + + const handleAddressLookup = async () => { + let latlng = await geocodeAddress( + newLocation !== undefined ? newLocation : parkingdata.Location, + newPostcode !== undefined ? newPostcode : parkingdata.Postcode, + newPlaats !== undefined ? newPlaats : parkingdata.Plaats + ); + if (false !== latlng) { + setNewCoordinaten(latlng.lat + "," + latlng.lon); + setCenterCoords(latlng.lat + "," + latlng.lon); + } else { + alert("Er is geen locatie beschikbaar voor dit adres. U kunt de locatie handmatig aanpassen."); + } + } + + const handleCoordinatesLookup = async () => { + let address = await reverseGeocode(newCoordinaten !== undefined ? newCoordinaten : parkingdata.Coordinaten); + if (address && address.address) { + let location = ((address.address.road || "---") + " " + (address.address.house_number || "")).trim(); + setNewLocation(location); + setNewPostcode(address.address.postcode); + setNewPlaats(address.address.city || address.address.town || address.address.village || address.address.quarter); + } else { + alert("Er is geen locatie beschikbaar voor dit adres. U kunt de locatie handmatig aanpassen."); + } + } + return ( -
+
@@ -375,6 +610,8 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD <> { setNewPostcode(e.target.value) }} value={newPostcode !== undefined ? newPostcode : parkingdata.Postcode} /> { setNewPlaats(e.target.value) }} value={newPlaats !== undefined ? newPlaats : parkingdata.Plaats} /> + {addressValid() && + }
@@ -421,7 +658,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD
- +
Verschuif de kaart om de coordinaten aan te passen @@ -444,26 +681,38 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD onChange={updateCoordinatesFromForm(false)} value={getCoordinate(false)} /> + {(newCoordinaten || !addressValid()) && + } + + {}
); } - const renderTabAfbeelding = () => { + const renderTabAfbeelding = (visible: boolean = false) => { return ( -
+
); } - const renderTabOpeningstijden = () => { + const renderTabOpeningstijden = (visible: boolean = false) => { const handlerSetNewOpening = (tijden: OpeningChangedType, Openingstijden: string): void => { setNewOpening(tijden); setNewOpeningstijden(Openingstijden); return; } return ( -
+
); } - const renderTabTarieven = () => { + const renderTabTarieven = (visible: boolean = false) => { return ( -
+
); @@ -497,7 +746,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD ); } - const renderTabCapaciteit = () => { + const renderTabCapaciteit = (visible: boolean = false) => { const handlerSetNewCapaciteit = (capaciteit: React.SetStateAction): void => { console.log('handlerSetNewCapaciteit', JSON.stringify(capaciteit, null, 2)); setNewCapaciteit(capaciteit); @@ -505,7 +754,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD } return ( -
+
{ + const renderTabAbonnementen = (visible: boolean = false) => { return ( -
+
); return ( @@ -538,40 +787,45 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD ); } - const renderTabBeheerder = () => { - let content; - - //console.log("#### got parkingdata", JSON.stringify(parkingdata, null, 2)); - - // console.log("### ParkingViewBeheerder", parkingdata, parkingdata.Exploitant, parkingdata.Beheerder, parkingdata.BeheerderContact); - if (parkingdata.FMS === true) { - content = FMS; - } else if (parkingdata?.exploitant) { - content = ( - - {parkingdata.exploitant.CompanyName} - + const renderTabBeheerder = (visible: boolean = false) => { + // TODO: uitzoeken & implementeren FMS / ExploitantID logica + if (parkingdata.FMS === true || parkingdata.ExploitantID !== null) { + return ( +
+
+
+ +
+

Wijzigen van de beheerder is op dit moment niet mogelijk

+
+
) - } else if (parkingdata.BeheerderContact !== null) { - content = ( - - {parkingdata.Beheerder === null ? 'contact' : parkingdata.Beheerder} - - ); - } else { - content = null } return ( -
- {content} +
+
+ +
+ { setNewBeheerder(e.target.value) }} value={newBeheerder !== undefined ? newBeheerder : parkingdata.Beheerder} /> + { setNewBeheerderContact(e.target.value) }} value={newBeheerderContact !== undefined ? newBeheerderContact : parkingdata.BeheerderContact} /> +
+
+
- ); + ) + } + + let parkingTitle = parkingdata.Title; + if (parkingdata.ID.substring(0, 8) === 'VOORSTEL') { + parkingTitle += " (voorstel)"; } + const parkingIDSet = parkingdata.ID === ""; + return (
-
{parkingdata.Title}
-
+ {showUpdateButtons === true && + Opslaan + } {showUpdateButtons === true && } + {showUpdateButtons === false && }
@@ -611,19 +875,20 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD + {/* */} - - - + {!parkingIDSet && } + {!parkingIDSet && } + {!parkingIDSet && } - {selectedTab === 'tab-algemeen' ? renderTabAlgemeen() : null} - {selectedTab === 'tab-afbeelding' ? renderTabAfbeelding() : null} - {selectedTab === 'tab-openingstijden' ? renderTabOpeningstijden() : null} - {selectedTab === 'tab-tarieven' ? renderTabTarieven() : null} - {selectedTab === 'tab-capaciteit' ? renderTabCapaciteit() : null} - {selectedTab === 'tab-abonnementen' ? renderTabAbonnementen() : null} - {selectedTab === 'tab-beheerder' ? renderTabBeheerder() : null} + {renderTabAlgemeen(selectedTab === 'tab-algemeen')} + {renderTabAfbeelding(selectedTab === 'tab-afbeelding')} + {renderTabOpeningstijden(selectedTab === 'tab-openingstijden')} + {renderTabTarieven(selectedTab === 'tab-tarieven')} + {renderTabCapaciteit(selectedTab === 'tab-capaciteit' && !parkingIDSet)} + {renderTabAbonnementen(selectedTab === 'tab-abonnementen' && !parkingIDSet)} + {renderTabBeheerder(selectedTab === 'tab-beheerder' && !parkingIDSet)}
); }; diff --git a/src/components/parking/ParkingEditLocation.tsx b/src/components/parking/ParkingEditLocation.tsx index 1de7141..2e182f9 100644 --- a/src/components/parking/ParkingEditLocation.tsx +++ b/src/components/parking/ParkingEditLocation.tsx @@ -76,6 +76,10 @@ function ParkingEditLocation({ } // } }); + mapboxMap.on('styleimagemissing', (e) => { + mapboxMap.addImage(e.id, { width: 0, height: 0, data: new Uint8Array(0) }); + }); + // Function that executes if component unloads: return () => { diff --git a/src/components/parking/ParkingViewOpening.tsx b/src/components/parking/ParkingViewOpening.tsx index 4002ff5..e98643d 100644 --- a/src/components/parking/ParkingViewOpening.tsx +++ b/src/components/parking/ParkingViewOpening.tsx @@ -5,28 +5,30 @@ import SectionBlock from "~/components/SectionBlock"; import HorizontalDivider from "~/components/HorizontalDivider"; const ParkingViewOpening = ({ parkingdata }: { parkingdata: any }) => { - if(parkingdata.Openingstijden!==null && parkingdata.Openingstijden.indexOf("\n")>-1) { + if (parkingdata.Openingstijden !== null && parkingdata.Openingstijden.indexOf("\n") > -1) { parkingdata.Openingstijden = parkingdata.Openingstijden.replace("\n", "
"); } + const isNS = parkingdata.EditorCreated === "NS-connector"; + return ( <> - {formatOpeningTimes(parkingdata, 2, "ma", "Maandag")} - {formatOpeningTimes(parkingdata, 3, "di", "Dinsdag")} - {formatOpeningTimes(parkingdata, 4, "wo", "Woensdag")} - {formatOpeningTimes(parkingdata, 5, "do", "Donderdag")} - {formatOpeningTimes(parkingdata, 6, "vr", "Vrijdag")} - {formatOpeningTimes(parkingdata, 0, "za", "Zaterdag")} - {formatOpeningTimes(parkingdata, 1, "zo", "Zondag")} + {formatOpeningTimes(parkingdata, 2, "ma", "Maandag", isNS)} + {formatOpeningTimes(parkingdata, 3, "di", "Dinsdag", isNS)} + {formatOpeningTimes(parkingdata, 4, "wo", "Woensdag", isNS)} + {formatOpeningTimes(parkingdata, 5, "do", "Donderdag", isNS)} + {formatOpeningTimes(parkingdata, 6, "vr", "Vrijdag", isNS)} + {formatOpeningTimes(parkingdata, 0, "za", "Zaterdag", isNS)} + {formatOpeningTimes(parkingdata, 1, "zo", "Zondag", isNS)} {parkingdata.Openingstijden !== "" && (

-
+
)} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 5646317..2c18dec 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,5 @@ import { SessionProvider } from "next-auth/react" +import { Toaster } from 'react-hot-toast'; import { type AppType } from "next/app"; import { wrapper } from "../store/store"; import Head from "next/head"; @@ -14,15 +15,16 @@ import "~/styles/globals.css"; import '~/styles/components/AppHeader.css'; -const MyApp: AppType = ({ Component, pageProps: {session, ...pageProps} }) => { +const MyApp: AppType = ({ Component, pageProps: { session, ...pageProps } }) => { return ( - <> - - -