diff --git a/.gitignore b/.gitignore index 411e1dc..6a6a385 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,7 @@ mysql-db/export-db.sql mysql-db/export-db.zip #uploads -/public/uploads* \ No newline at end of file +/public/uploads* + +#auto generated test scripts +create-test-user.sql \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fb02753..4a96b12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "@trpc/server": "^10.18.0", "@turf/center": "^6.5.0", "@turf/turf": "^6.5.0", + "@types/moment": "^2.13.0", + "@types/moment-timezone": "^0.5.30", "bcrypt": "^5.1.1", "formidable": "^3.5.1", "keen-slider": "^6.8.5", @@ -32,6 +34,7 @@ "mapbox-gl-utils": "^0.39.0", "maplibre-gl": "^2.0.0-pre.1", "mime": "^3.0.0", + "moment-timezone": "^0.5.45", "next": "^13.4.18", "next-auth": "^4.23.1", "next-pwa": "^5.6.0", @@ -42,6 +45,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^4.8.0", "react-redux": "^8.0.5", + "react-table": "^7.8.0", "superjson": "1.12.2", "zod": "^3.21.4" }, @@ -57,6 +61,7 @@ "@types/prettier": "^2.7.2", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", + "@types/react-table": "^7.7.20", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "autoprefixer": "^10.4.14", @@ -5133,6 +5138,24 @@ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, + "node_modules/@types/moment": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz", + "integrity": "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ==", + "deprecated": "This is a stub types definition for Moment (https://github.com/moment/moment). Moment provides its own type definitions, so you don't need @types/moment installed!", + "dependencies": { + "moment": "*" + } + }, + "node_modules/@types/moment-timezone": { + "version": "0.5.30", + "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.30.tgz", + "integrity": "sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg==", + "deprecated": "This is a stub types definition. moment-timezone provides its own type definitions, so you do not need this installed.", + "dependencies": { + "moment-timezone": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -5206,6 +5229,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-table": { + "version": "7.7.20", + "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.20.tgz", + "integrity": "sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -9909,6 +9941,25 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11414,6 +11465,18 @@ } } }, + "node_modules/react-table": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", + "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index a916a97..57875a4 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "@trpc/server": "^10.18.0", "@turf/center": "^6.5.0", "@turf/turf": "^6.5.0", + "@types/moment": "^2.13.0", + "@types/moment-timezone": "^0.5.30", "bcrypt": "^5.1.1", "formidable": "^3.5.1", "keen-slider": "^6.8.5", @@ -40,6 +42,7 @@ "mapbox-gl-utils": "^0.39.0", "maplibre-gl": "^2.0.0-pre.1", "mime": "^3.0.0", + "moment-timezone": "^0.5.45", "next": "^13.4.18", "next-auth": "^4.23.1", "next-pwa": "^5.6.0", @@ -50,6 +53,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^4.8.0", "react-redux": "^8.0.5", + "react-table": "^7.8.0", "superjson": "1.12.2", "zod": "^3.21.4" }, @@ -65,6 +69,7 @@ "@types/prettier": "^2.7.2", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", + "@types/react-table": "^7.7.20", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "autoprefixer": "^10.4.14", diff --git a/src/components/Parking.tsx b/src/components/Parking.tsx index 1caa02f..fd9f8c9 100644 --- a/src/components/Parking.tsx +++ b/src/components/Parking.tsx @@ -25,6 +25,7 @@ const Parking = ({ id, stallingId, onStallingIdChanged, onClose }: { id: string, if (null !== stalling) { setCurrentStalling(stalling); } else { + console.error("Failed to load stalling with ID: " + stallingId); setCurrentStalling(null); } }); diff --git a/src/components/ParkingFacilityBlock.tsx b/src/components/ParkingFacilityBlock.tsx index 94a74f2..4017da7 100644 --- a/src/components/ParkingFacilityBlock.tsx +++ b/src/components/ParkingFacilityBlock.tsx @@ -1,97 +1,13 @@ import { useRouter } from "next/navigation"; +import moment from "moment"; import { getParkingColor } from "~/utils/theme"; import { openRoute } from "~/utils/map/index"; -import Styles from "./ParkingFacilityBlock.module.css"; - -type ParkingType = { - ID: string; - Title: string; - Plaats?: string; - Location?: string; - Postcode?: any; - Status?: any; - Coordinaten?: any; - Type?: any; - Tariefcode?: number; - Openingstijden?: string; - Open_ma?: string; - Dicht_ma?: string; - Open_di?: string; - Dicht_di?: string; - Open_wo?: string; - Dicht_wo?: string; - Open_do?: string; - Dicht_do?: string; - Open_vr?: string; - Dicht_vr?: string; - Open_za?: string; - Dicht_za?: string; - Open_zo?: string; - Dicht_zo?: string; - Image?: string; - ExtraServices?: string; -} - -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() - 1 * 60 + openingTime.getMinutes();//TODO - let closing = closingTime.getHours() - 1 * 60 + closingTime.getMinutes();//TODO - - // #TODO: Combine functions with /src/utils/parkings.tsx - if (opening === closing && opening === 60 && closing === 60) { - // 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) { - // Closing time is on the next day, add 24 hours to closing time - closing += 24 * 60; - } - - return currentTime >= opening && currentTime <= closing; -}; - -const formatTime = (time: Date): string => { - const hours = (time.getHours() - 1).toString().padStart(2, "0");//TODO - const minutes = time.getMinutes().toString().padStart(2, "0"); - return `${hours}:${minutes}`; -}; +import { formatOpeningToday } from "~/utils/parkings-openclose"; +import type { ParkingDetailsType } from "~/types/"; -const formatOpeningToday = (parkingdata: any): string => { - const dayidx = new Date().getDay(); - const daytxt = ["za", "zo", "ma", "di", "wo", "do", "vr"]; - - const openstr = parkingdata["Open_" + daytxt[dayidx]]; - const closestr = parkingdata["Dicht_" + daytxt[dayidx]]; - - if (null === openstr || null === closestr) { - return ""; - } - - const openinfo = new Date(openstr); - const closeinfo = new Date(closestr); - - const isNS = parkingdata.EditorCreated === "NS-connector"; - if (isOpen(openinfo, closeinfo, isNS)) { - let str = `open`; - - // Exception: If this is a 24/h a day - // NS parking -> don't show "until ..." - if (openstr === closestr) { - return str; - } - - str += `, sluit om ${formatTime(closeinfo)}`; - - return str; - } else { - return "gesloten"; - } -}; +import Styles from "./ParkingFacilityBlock.module.css"; function ParkingFacilityBlock({ parking, @@ -102,7 +18,7 @@ function ParkingFacilityBlock({ showButtons, }: { id?: any, - parking: ParkingType, + parking: ParkingDetailsType, compact: boolean openParkingHandler?: Function, expandParkingHandler?: Function, @@ -139,7 +55,7 @@ function ParkingFacilityBlock({ break; } - const openingDescription = formatOpeningToday(parking); + const openingDescription = formatOpeningToday(parking, moment()).message; // const detailsLine = `${costDescription}${costDescription && openingDescription ? "| " : "" // }${openingDescription}`; diff --git a/src/components/parking/ParkingEdit.tsx b/src/components/parking/ParkingEdit.tsx index 0db643a..d823af5 100644 --- a/src/components/parking/ParkingEdit.tsx +++ b/src/components/parking/ParkingEdit.tsx @@ -109,7 +109,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD const [newServices, setNewServices] = React.useState([]); const [newCapaciteit, setNewCapaciteit] = React.useState([]); // capaciteitschema - const [newOpening, setNewOpening] = React.useState(undefined); // openingstijdenschema + const [newOpening, setNewOpening] = React.useState(undefined); // openingstijdenschema const [newOpeningstijden, setNewOpeningstijden] = React.useState(undefined); // textveld afwijkende openingstijden type StallingType = { id: string, name: string, sequence: number }; @@ -275,7 +275,13 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD if (undefined !== newOpening) { for (const keystr in newOpening) { const key = keystr as keyof ParkingEditUpdateStructure; - update[key] = new Date(newOpening[key]).toISOString(); + if (newOpening[key] === null) { + update[key] = null + } else if (newOpening[key] !== undefined) { + update[key] = newOpening[key].format("YYYY-MM-DDTHH:mm:ss.SSS[Z]"); + } else { + // do nothing + } } } @@ -757,6 +763,7 @@ const ParkingEdit = ({ parkingdata, onClose, onChange }: { parkingdata: ParkingD const renderTabOpeningstijden = (visible: boolean = false) => { const handlerSetNewOpening = (tijden: OpeningChangedType, Openingstijden: string): void => { + console.log("set new opening", tijden, Openingstijden); setNewOpening(tijden); setNewOpeningstijden(Openingstijden); return; diff --git a/src/components/parking/ParkingEditOpening.tsx b/src/components/parking/ParkingEditOpening.tsx index 1a29971..3c31edd 100644 --- a/src/components/parking/ParkingEditOpening.tsx +++ b/src/components/parking/ParkingEditOpening.tsx @@ -7,6 +7,8 @@ import FormInput from "~/components/Form/FormInput"; import FormTextArea from "~/components/Form/FormTextArea"; import FormCheckbox from "~/components/Form/FormCheckbox"; +import moment from "moment"; + type OpeningDetailsType = { Open_ma: Date, Dicht_ma: Date, @@ -25,7 +27,7 @@ type OpeningDetailsType = { } export type OpeningChangedType = { - [key: string]: Date + [key: string]: moment.Moment | null } const getOpenTimeKey = (day: DayPrefix): keyof OpeningDetailsType => { @@ -38,6 +40,7 @@ const getDichtTimeKey = (day: DayPrefix): keyof OpeningDetailsType => { const formatOpeningTimesForEdit = ( parkingdata: OpeningDetailsType, + isNS: boolean, day: DayPrefix, label: string, handlerChange: Function, @@ -45,35 +48,48 @@ const formatOpeningTimesForEdit = ( ): React.ReactNode => { const wkday = new Date().getDay(); - const tmpopen: Date = new Date(parkingdata[getOpenTimeKey(day)]); - const hoursopen = tmpopen.getHours() - 1;//TODO - const minutesopen = String(tmpopen.getMinutes()).padStart(2, "0"); - - const tmpclose: Date = new Date(parkingdata[getDichtTimeKey(day)]); - const hoursclose = tmpclose.getHours() - 1;//TODO - const minutesclose = String(tmpclose.getMinutes()).padStart(2, "0"); - - let value = `${hoursopen}:${minutesopen} - ${hoursclose}:${minutesclose}`; - - let diff = Math.abs((tmpclose.getTime() - tmpopen.getTime()) / 1000); - if (diff >= 86340) { - value = '24h' - } else if (diff === 0) { - value = 'gesloten' + const opentime = parkingdata[getOpenTimeKey(day)]; + const tmpopen = moment.utc(opentime); + const hoursopen = tmpopen.hours(); + const minutesopen = String(tmpopen.minutes()).padStart(2, "0"); + + const closetime = parkingdata[getDichtTimeKey(day)]; + const tmpclose = moment.utc(closetime); + const hoursclose = tmpclose.hours(); + const minutesclose = String(tmpclose.minutes()).padStart(2, "0"); + + let onbekend = false; + let gesloten = false; + let open24h = false; + + if (isNS) { + onbekend = opentime === null && closetime === null; + open24h = tmpopen.hours() === 0 && tmpopen.minutes() === 0 && tmpopen.hours() === 0 && tmpopen.minutes() === 0; + gesloten = false; + } else { + onbekend = opentime === null && closetime === null; + open24h = !onbekend && (tmpopen.hours() === 0 && tmpopen.minutes() === 0 && tmpclose.hours() === 23 && tmpclose.minutes() === 59); + gesloten = !onbekend && (tmpopen.hours() === 0 && tmpopen.minutes() === 0 && tmpclose.hours() === 0 && tmpclose.minutes() === 0); } - const showtimes = diff > 0 && diff < 86340; + const showtimes = !(open24h || gesloten || onbekend); + return ( {label} - = 86340} onChange={handlerChangeChecks(day, true)}> + 24h - + {isNS === false && gesloten + } + + + + onbekend @@ -119,7 +135,6 @@ const formatOpeningTimesForEdit = ( : null} - [{value}] ); }; @@ -143,28 +158,29 @@ const extractParkingFields = (parkingdata: ParkingDetailsType): OpeningDetailsTy } } -const setHourInDate = (date: Date, newHour: number): Date => { +const setHourInDate = (date: moment.Moment, newHour: number): moment.Moment => { if (newHour < 0 || newHour >= 24) { throw new Error('Invalid hour value. Hour should be between 0 and 23.'); } - const newDate = new Date(date); - newDate.setHours(newHour); + const newDate = date.clone(); + newDate.hours(newHour); return newDate; }; -const setMinutesInDate = (date: Date, newMinutes: number): Date => { +const setMinutesInDate = (date: moment.Moment, newMinutes: number): moment.Moment => { if (newMinutes < 0 || newMinutes >= 60) { throw new Error('Invalid minutes value. Minutes should be between 0 and 59.'); } - const newDate = new Date(date); - newDate.setMinutes(newMinutes); + const newDate = date.clone(); + newDate.minutes(newMinutes); return newDate; }; -const ParkingEditOpening = ({ parkingdata, openingChanged }: { parkingdata: any, openingChanged: Function }) => { +const ParkingEditOpening = ({ parkingdata, openingChanged }: { parkingdata: ParkingDetailsType, openingChanged: Function }) => { const startValues = extractParkingFields(parkingdata); + const isNS = parkingdata.EditorCreated === "NS-connector"; const [changes, setChanges] = useState({}); const [openingstijden, setOpeningstijden] = useState(undefined); @@ -184,7 +200,7 @@ const ParkingEditOpening = ({ parkingdata, openingChanged }: { parkingdata: any, // determine new time // let oldtime: Date = new Date((key in currentValues) ? currentValues[key]: startValues[key]); - let oldtime: Date = new Date((key in changes) ? changes[key] as Date : startValues[key]); + let oldtime = moment.utc((key in changes) ? changes[key] : startValues[key]); let newtime = undefined; const newval: number = Number(e.target.value); @@ -207,23 +223,38 @@ const ParkingEditOpening = ({ parkingdata, openingChanged }: { parkingdata: any, } // Function that runs if the active state changes - const handleChangeChecks = (day: DayPrefix, is24hourscheck: boolean) => (e: React.ChangeEvent) => { + const handleChangeChecks = (day: DayPrefix, whichcheck: "open24" | "gesloten" | "onbekend") => (e: React.ChangeEvent) => { const openkey = getOpenTimeKey(day) const dichtkey = getDichtTimeKey(day); - if (e.target.checked) { - const newopen = new Date(0); - - // add 24 hours for full day open, otherwise 0 for full day closed - const newdicht = new Date(is24hourscheck ? (86340 * 1000) : 0); + let newopen: moment.Moment | null = null; + let newdicht: moment.Moment | null = null; - setChanges({ ...changes, [openkey]: newopen, [dichtkey]: newdicht }); + if (e.target.checked) { + switch (whichcheck) { + case "open24": + if (!isNS) { + newopen = moment.utc(0); + newdicht = setMinutesInDate(setHourInDate(moment.utc(0), 23), 59); + } else { + newopen = moment.utc(0); + newdicht = moment.utc(0); + } + break; + case "gesloten": + newopen = moment.utc(0); + newdicht = moment.utc(0); + break; + case "onbekend": + newopen = null; + newdicht = null; + break; + } } else { - const newopen = setHourInDate(new Date(0), 10); - const newdicht = setHourInDate(new Date(0), 17); - - setChanges({ ...changes, [openkey]: newopen, [dichtkey]: newdicht }); + newopen = setHourInDate(moment.utc(0), 10); + newdicht = setHourInDate(moment.utc(0), 17); } + setChanges({ ...changes, [openkey]: newopen, [dichtkey]: newdicht }); } // Function that runs if extra description field changes @@ -250,13 +281,13 @@ const ParkingEditOpening = ({ parkingdata, openingChanged }: { parkingdata: any,

*/} - {formatOpeningTimesForEdit(data, "ma", "Maandag", handleChange, handleChangeChecks)} - {formatOpeningTimesForEdit(data, "di", "Dinsdag", handleChange, handleChangeChecks)} - {formatOpeningTimesForEdit(data, "wo", "Woensdag", handleChange, handleChangeChecks)} - {formatOpeningTimesForEdit(data, "do", "Donderdag", handleChange, handleChangeChecks)} - {formatOpeningTimesForEdit(data, "vr", "Vrijdag", handleChange, handleChangeChecks)} - {formatOpeningTimesForEdit(data, "za", "Zaterdag", handleChange, handleChangeChecks)} - {formatOpeningTimesForEdit(data, "zo", "Zondag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "ma", "Maandag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "di", "Dinsdag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "wo", "Woensdag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "do", "Donderdag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "vr", "Vrijdag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "za", "Zaterdag", handleChange, handleChangeChecks)} + {formatOpeningTimesForEdit(data, isNS, "zo", "Zondag", handleChange, handleChangeChecks)}
diff --git a/src/components/parking/ParkingViewAbonnementen.tsx b/src/components/parking/ParkingViewAbonnementen.tsx index f035aac..baad584 100644 --- a/src/components/parking/ParkingViewAbonnementen.tsx +++ b/src/components/parking/ParkingViewAbonnementen.tsx @@ -5,21 +5,101 @@ import HorizontalDivider from "~/components/HorizontalDivider"; import { Button } from "~/components/Button"; import SectionBlock from "~/components/SectionBlock"; import { ParkingDetailsType } from "~/types"; +import { getMunicipalityBasedOnLatLng } from "~/utils/map/active_municipality"; const ParkingViewAbonnementen = ({ parkingdata }: { parkingdata: ParkingDetailsType }) => { + const [abonnementUrl, setAbonnementUrl] = useState(""); + useEffect(() => { + updateAbonnementUrl(parkingdata); + }, [parkingdata]); // if(!parkingdata.abonnementsvorm_fietsenstalling || parkingdata.abonnementsvorm_fietsenstalling.length === 0) { // return null; // } - const activeMunicipalityInfo = useSelector( - (state: any) => state.map.activeMunicipalityInfo - ); - - // console.log('activeMunicipalityInfo', activeMunicipalityInfo); // parkingdata.abonnementsvorm_fietsenstalling.map(x => console.log('abonnement', x)); // console.log("abonnementsvormen", JSON.stringify(parkingdata.abonnementsvorm_fietsenstalling, null, 2)); + const updateAbonnementUrl = async (parkingdata: ParkingDetailsType): Promise => { + {/* + example of original url for redirect to fietskluizen abbo referral: + https://veiligstallen.nl/molenlanden/fietskluizen/4201_002#4201_002 + + + + + + + + + + + + + + + + + base///# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + council = contacts table + + */} + + let url = ""; + const stallingMunicipalty = await getMunicipalityBasedOnLatLng(parkingdata.Coordinaten.split(",")); + if (stallingMunicipalty) { + switch (parkingdata.Type) { + case "fietskluizen": + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/fietskluizen/${parkingdata.StallingsID}`; + break; + case "fietstrommel": + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/fietstrommels/${parkingdata.StallingsID}`; + break; + case "buurtstalling": + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/buurtstallingen/${parkingdata.StallingsID}`; + break; + default: + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/stallingen/${parkingdata.StallingsID}#${parkingdata.StallingsID}`; + break; + } + } + setAbonnementUrl(url); + } + return ( <> @@ -34,7 +114,7 @@ const ParkingViewAbonnementen = ({ parkingdata }: { parkingdata: ParkingDetailsT {((parkingdata.abonnementsvorm_fietsenstalling && parkingdata.abonnementsvorm_fietsenstalling.length > 0)) ?
diff --git a/src/components/parking/ParkingViewOpening.tsx b/src/components/parking/ParkingViewOpening.tsx index e98643d..2bc9f1d 100644 --- a/src/components/parking/ParkingViewOpening.tsx +++ b/src/components/parking/ParkingViewOpening.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { formatOpeningTimes } from "~/utils/parkings"; +import { formatOpeningTimes } from "~/utils/parkings-openclose"; +import moment from "moment"; import SectionBlock from "~/components/SectionBlock"; import HorizontalDivider from "~/components/HorizontalDivider"; @@ -10,6 +11,7 @@ const ParkingViewOpening = ({ parkingdata }: { parkingdata: any }) => { } const isNS = parkingdata.EditorCreated === "NS-connector"; + const wkday = moment().day(); return ( <> @@ -17,13 +19,13 @@ const ParkingViewOpening = ({ parkingdata }: { parkingdata: any }) => { heading="Openingstijden" contentClasses="grid grid-cols-2" > - {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)} + {formatOpeningTimes(parkingdata, "ma", "Maandag", wkday === 1, isNS)} + {formatOpeningTimes(parkingdata, "di", "Dinsdag", wkday === 2, isNS)} + {formatOpeningTimes(parkingdata, "wo", "Woensdag", wkday === 3, isNS)} + {formatOpeningTimes(parkingdata, "do", "Donderdag", wkday === 4, isNS)} + {formatOpeningTimes(parkingdata, "vr", "Vrijdag", wkday === 5, isNS)} + {formatOpeningTimes(parkingdata, "za", "Zaterdag", wkday === 6, isNS)} + {formatOpeningTimes(parkingdata, "zo", "Zondag", wkday === 0, isNS)} {parkingdata.Openingstijden !== "" && (
diff --git a/src/pages/ns/stallingen/[id].tsx b/src/pages/ns/stallingen/[id].tsx new file mode 100644 index 0000000..35fc31b --- /dev/null +++ b/src/pages/ns/stallingen/[id].tsx @@ -0,0 +1,46 @@ +/* This page is used to redirect the user for old style NS links + in the format https://www.veiligstallen.nl/ns/stallingen/[id] */ + +import { getParkingsFromDatabase } from "~/utils/prisma"; +import { GetServerSideProps } from 'next'; + +export const getServerSideProps: GetServerSideProps = async (context) => { + + const id = context.query?.id || false; + if (!id) { + // redirect to /, no id given; + return { + redirect: { + destination: `/`, + permanent: true, + }, + }; + } + + const stallingen = await getParkingsFromDatabase([], null); + // convert NS stalling code to internal ID + const newstallingen = stallingen.filter((stalling) => stalling.StallingsID == id); + if (newstallingen.length === 1 && newstallingen[0] !== undefined) { + // redirect to / for given stalling ID + return { + redirect: { + destination: `/?stallingid=${newstallingen[0].ID}`, + permanent: false, + }, + }; + } else { + // redirect to /, stalling not found + return { + redirect: { + destination: `/`, + permanent: true, + }, + }; + } +}; + +const RedirectPage = () => { + return null; +}; + +export default RedirectPage; \ No newline at end of file diff --git a/src/pages/reports/index.tsx b/src/pages/reports/index.tsx new file mode 100644 index 0000000..1bbf857 --- /dev/null +++ b/src/pages/reports/index.tsx @@ -0,0 +1,288 @@ +import React, { useRef, useState, useEffect } from "react"; +import { NextPage } from "next/types"; +import { GetServerSidePropsContext } from 'next'; +import { getServerSession } from "next-auth/next" +import { authOptions } from '~/pages/api/auth/[...nextauth]' +import type { fietsenstallingen } from "@prisma/client"; +import moment from "moment"; + +import { getParkingsFromDatabase } from "~/utils/prisma"; +import { getMunicipalities } from "~/utils/municipality"; + +import ReportTable from "~/utils/reports/report-table"; +import { noReport, type ReportContent } from "~/utils/reports/types"; +import { createOpeningTimesReport } from "~/utils/reports/openingtimes"; +import { createFixBadDataReport } from "~/utils/reports/baddata"; + +export async function getServerSideProps(context: GetServerSidePropsContext) { + try { + const session = await getServerSession(context.req, context.res, authOptions) + const fietsenstallingen = await getParkingsFromDatabase([], session); + + return { + props: { + fietsenstallingen, + user: session?.user?.email || false, + }, + }; + } catch (ex: any) { + return { + props: { + fietsenstallingen: [], + user: false + }, + }; + } +} + +const downloadCSV = (csv: string, filename: string) => { + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + if (link.download !== undefined) { + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', filename); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } +}; + +const extractText = (element: React.ReactNode): string => { + if (typeof element === 'string') { + return element; + } + if (typeof element === 'number') { + return element.toString(); + } + if (React.isValidElement(element)) { + const children = element.props.children; + if (Array.isArray(children)) { + return children.map(extractText).join(''); + } + return extractText(children); + } + return ''; +}; + +const convertToCSV = (objArray: any[], columns: string[], hiddenColumns: string[]): string => { + const visibleColumns = columns.filter(col => !hiddenColumns.includes(col)); + const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray; + let str = visibleColumns.join(',') + '\r\n'; + + for (let i = 0; i < array.length; i++) { + let line = ''; + for (let index in visibleColumns) { + if (line !== '') line += ','; + line += extractText(array[i][visibleColumns[index] as string]); + } + str += line + '\r\n'; + } + + return str; +}; + +const Report: NextPage = ({ fietsenstallingen }: any) => { + let filterSettings = { + selectedReport: 'openclose', + filterType: '', + isNs: 'all', + filterDateTime: moment().format('YYYY-MM-DDTHH:mm'), + showData: true + }; + + if (typeof window !== 'undefined') { + const jsonfilterSettings = localStorage.getItem('filterSetings'); + if (null !== jsonfilterSettings) { + filterSettings = Object.assign(filterSettings, JSON.parse(jsonfilterSettings)); + } + } + + const [selectedReport, setSelectedReport] = useState(filterSettings.selectedReport); + const [filterType, setFilterType] = useState(filterSettings.filterType); + const [filterDateTime, setFilterDateTime] = useState(filterSettings.filterDateTime); + const [isNs, setIsNs] = useState(filterSettings.isNs); + const [showData, setShowData] = useState(filterSettings.showData); + + const [reportContent, setReportContent] = useState(noReport); + const [loading, setLoading] = useState(false); + + const [contacts, setContacts] = useState(undefined); + + const abortControllerRef = useRef(null); + + const launchReport = (createReportFunction: (filtered: any) => ReportContent) => { + setLoading(true); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + setTimeout(() => { + if (signal.aborted) return; + + const filtered = fietsenstallingen.filter((parkingdata: fietsenstallingen) => { + return (filterType === '' || parkingdata.Type === filterType) && + (isNs === 'all' || (isNs === 'true' && parkingdata.EditorCreated === "NS-connector") || (isNs === 'false' && parkingdata.EditorCreated !== "NS-connector")); + }); + + setReportContent(createReportFunction(filtered)); + + setLoading(false); + }, 500); + } + + useEffect(() => { + const go = async () => { + const response = await fetch(`/api/contacts`); + setContacts(await response.json()); + }; + + go(); + }, []); + + useEffect(() => { + const filterSettings = { + selectedReport, + filterType, + isNs, + filterDateTime, + showData + } + localStorage.setItem('filterSetings', JSON.stringify(filterSettings)); + switch (selectedReport) { + case 'openclose': + launchReport((filtered: any): ReportContent => { + return createOpeningTimesReport(filtered, moment(filterDateTime), showData); + }); + break; + case 'baddata': + launchReport((filtered: any): ReportContent => { + return createFixBadDataReport(filtered, contacts, showData); + }); + break; + default: + launchReport((filtered: any): ReportContent => { + return noReport; + }); + break; + } + }, [selectedReport, filterType, isNs, filterDateTime, showData, contacts]); + + + const handleDownloadCSV = () => { + if (reportContent) { + const csv = convertToCSV(reportContent.data.records, reportContent.data.columns, reportContent.data.hidden || []); + downloadCSV(csv, 'report.csv'); + } + }; + + const handleReportSelection = (event: React.ChangeEvent) => { + setSelectedReport(event.target.value); + }; + + const handleFilterTypeChange = (event: React.ChangeEvent) => { + setFilterType(event.target.value); + }; + + const handleIsNsChange = (event: React.ChangeEvent) => { + setIsNs(event.target.value); + }; + + const handleFilterDateTimeChange = (event: React.ChangeEvent) => { + setFilterDateTime(event.target.value); + }; + + const handleShowDataChange = (event: React.ChangeEvent) => { + setShowData(event.target.checked); + }; + + const availabletypes = fietsenstallingen.reduce((acc: string[], parkingdata: fietsenstallingen) => { + const type = parkingdata.Type || "unknown"; + if (!acc.includes(type)) { + acc.push(type); + } + return acc; + }, []); + + return ( +
+
+
+ + + + + + + + + +
+ +
+ + {loading && ( +
+ + + + + Loading... +
+ )} + {!loading && reportContent && +
+ +
} +
+ ); +}; + +export default Report; diff --git a/src/pages/reports/report.module.css b/src/pages/reports/report.module.css new file mode 100644 index 0000000..c7d50ca --- /dev/null +++ b/src/pages/reports/report.module.css @@ -0,0 +1,33 @@ +.Report_Body h2 { + font-size: 1.1em; + font-weight: bold; +} + +.Report_Body ul { + list-style-type: disc; +} + +.Report_Body ul, +.Report_Body ol { + margin: 1em 0; + padding: 0 0 0 40px; + margin-left: 0; + padding-left: 1em; +} + +.Report_Body li { + display: list-item; +} + +.Report_Body a { + text-decoration: underline; +} + +.Report_Body strong { + font-weight: bold; +} + +.Report_Body p { + margin-top: 5px; + margin-bottom: 15px; +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index b319004..561142b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -29,12 +29,14 @@ export type ParkingSections = ParkingSection[]; export type ParkingDetailsType = { ID: string, Status: string, + EditorCreated: string, Title: string, Location: string, Postcode: string, Plaats: string, Type: string, SiteID: string, + StallingsID: string, Description: string; Image: any; Open_ma: Date, @@ -67,6 +69,8 @@ export type ParkingDetailsType = { BikeparkID: string, abonnementsvormen: abonnementsvormen }[], + Tariefcode: number, + ExtraServices: string, // abonnementsvormen: { // ID: string, // naam: string, diff --git a/src/utils/parkings-openclose.tsx b/src/utils/parkings-openclose.tsx new file mode 100644 index 0000000..98a7136 --- /dev/null +++ b/src/utils/parkings-openclose.tsx @@ -0,0 +1,211 @@ +import React from "react"; +import moment from "moment"; +import { getMunicipalityBasedOnLatLng } from "~/utils/map/active_municipality"; + +import type { ParkingDetailsType, DayPrefix } from "~/types/"; + +const getOpenTimeKey = (day: DayPrefix): keyof ParkingDetailsType => { + return ('Open_' + day) as keyof ParkingDetailsType; +} + +const getDichtTimeKey = (day: DayPrefix): keyof ParkingDetailsType => { + return ('Dicht_' + day) as keyof ParkingDetailsType; +} + +export const formatTime = (time: moment.Moment): string => { + return time.format('HH:mm'); +}; + +const getExceptionTypes = () => { + return [ + "fietstrommel", + "fietskluizen", + "buurtstalling" + ] +} + +export type openingTodayType = { + isOpen: boolean | undefined, + message: string +} + +export const formatOpeningToday = (parkingdata: ParkingDetailsType, thedate: moment.Moment): openingTodayType => { + const dayidx = thedate.day(); + const daytxt = ["zo", "ma", "di", "wo", "do", "vr", "za"][dayidx] as DayPrefix; + + const opentime = parkingdata[getOpenTimeKey(daytxt)]; + const closetime = parkingdata[getDichtTimeKey(daytxt)]; + + const openinfo = moment.utc(opentime); + const closeinfo = moment.utc(closetime); + + + const isNS = parkingdata.EditorCreated === "NS-connector"; + + // handle exceptions + let result = undefined; + if (getExceptionTypes().includes(parkingdata.Type)) { + result = { isOpen: undefined, message: "" }; // no opening times + } else if (null === opentime || null === closetime) { + result = { isOpen: undefined, message: "" }; // undefined + } else { + if (openinfo.hours() === 0 && openinfo.minutes() === 0 && closeinfo.hours() === 23 && closeinfo.minutes() === 59) { + result = { isOpen: true, message: '24 uur open' } + } + else if (openinfo.hours() === 0 && openinfo.minutes() === 0 && closeinfo.hours() === 0 && closeinfo.minutes() === 0) { // Exception for NS parkings: If NS parking AND open from 1am to 1am, + // then the parking is open 24 hours per day. + if (isNS) { + result = { isOpen: true, message: '24 uur open' } + } else { + result = { isOpen: false, message: 'gesloten' } + } + } + } + + if (undefined !== result) { + return result + } + + const currentMinutes = thedate.hours() * 60 + thedate.minutes(); + const openingMinutes = openinfo.hours() * 60 + openinfo.minutes(); + let closingMinutes = closeinfo.hours() * 60 + closeinfo.minutes(); + if (closingMinutes < openingMinutes) { + // Closing time is on the next day, add 24 hours to closing time + closingMinutes += 24 * 60; + } + + let isOpen = currentMinutes >= openingMinutes && currentMinutes <= closingMinutes; + if (openinfo.hours() === closingMinutes && openingMinutes === 60 && closingMinutes === 60) { + isOpen = isNS; + } + else if (openinfo.hours() === 0 && openinfo.minutes() === 0 && closeinfo.hours() === 23 && closeinfo.minutes() === 59) { + isOpen = true; + } + + if (isOpen) { + let str = `open`; + + // Exception: If this is a 24/h a day + // NS parking -> don't show "until ..." + if (opentime !== closetime) { // ||(openinfo==="00:00" && closeinfo==="23:59") + str += `, sluit om ${formatTime(closeinfo)}`; + } + + result = { isOpen: true, message: str }; + } else { + result = { isOpen: false, message: "gesloten" }; + } + + if (result.isOpen === false) { + // Extra check: see if the current time is part of yesterdays opening times + const yesterdayidx = thedate.day() === 0 ? 6 : thedate.day() - 1; + const yesterdaytxt = ["zo", "ma", "di", "wo", "do", "vr", "za"][yesterdayidx] as DayPrefix; + + const y_opentime = parkingdata[getOpenTimeKey(yesterdaytxt)] + const y_closetime = parkingdata[getDichtTimeKey(yesterdaytxt)] + + if (null !== y_opentime && null !== y_closetime) { + const y_openinfo = moment.utc(y_opentime); + const y_closeinfo = moment.utc(y_closetime); + + const y_openingMinutes = y_openinfo.hours() * 60 + y_openinfo.minutes(); + const y_closingMinutes = y_closeinfo.hours() * 60 + y_closeinfo.minutes(); + + // const exception = + // y_openingMinutes === 0 && y_closingMinutes === 0 || + // y_openingMinutes === 0 && y_closingMinutes === 60*23 + 59 + // never applies when condition below is true + + if (y_closingMinutes < y_openingMinutes && // closing time wraps to today + currentMinutes >= 0 && + currentMinutes < y_closingMinutes) { + // open when current time is between 0:00 and yesterdays closing time + result.isOpen = true; + result.message = "open"; + // Exception: If this is a 24/h a day + // NS parking -> don't show "until ..." + if (opentime !== closetime) { + result.message += `, sluit om ${formatTime(y_closeinfo)}`; + } + } + } + } + + return result; +}; + +export const formatOpeningTimes = ( + parkingdata: ParkingDetailsType, + day: DayPrefix, + label: string, + bold: boolean, + isNS: boolean = false +): React.ReactNode => { + const opentime = parkingdata[getOpenTimeKey(day)]; + const closetime = parkingdata[getDichtTimeKey(day)]; + const tmpopen = moment.utc(opentime); + const hoursopen = tmpopen.hours(); + const minutesopen = String(tmpopen.minutes()).padStart(2, "0"); + + const tmpclose = moment.utc(closetime); + const hoursclose = tmpclose.hours(); + const minutesclose = String(tmpclose.minutes()).padStart(2, "0"); + + let value = `${hoursopen}:${minutesopen} - ${hoursclose}:${minutesclose}`; + + if (getExceptionTypes().includes(parkingdata.Type)) { + return null; // no opening times + } else if (null === opentime || null === closetime) { + value = "Onbekend"; // onbekend + } + else if (hoursopen === 0 && minutesopen === "00" && hoursclose === 23 && minutesclose === "59") { + value = '24h' + } + else if (hoursopen === 0 && minutesopen === "00" && hoursclose === 0 && minutesclose === "00") { // Exception for NS parkings: If NS parking AND open from 1am to 1am, + // then the parking is open 24 hours per day. + if (isNS) { + value = '24h'; + } else { + value = 'gesloten'; + } + } + + return ( + <> +
{label}
+
{value}
+ + ); +}; + +export const createVeilistallenOrgLink = async (parkingdata: ParkingDetailsType): Promise => { + let url = ''; + if (parkingdata.EditorCreated === "NS-connector") { + url = 'https://www.veiligstallen.nl/ns/stallingen/uto002#uto002' + } else { + if (!parkingdata.Coordinaten || parkingdata.Coordinaten === "") { + // no municipality available + return "" + } + const stallingMunicipalty = await getMunicipalityBasedOnLatLng(parkingdata.Coordinaten.split(",")); + if (stallingMunicipalty) { + switch (parkingdata.Type) { + case "fietskluizen": + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/fietskluizen/${parkingdata.StallingsID}`; + break; + case "fietstrommel": + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/fietstrommels/${parkingdata.StallingsID}`; + break; + case "buurtstalling": + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/buurtstallingen/${parkingdata.StallingsID}`; + break; + default: + url = `https://veiligstallen.nl/${stallingMunicipalty.name}/stallingen/${parkingdata.StallingsID}#${parkingdata.StallingsID}`; + break; + } + } + } + + return url; +} + diff --git a/src/utils/parkings.tsx b/src/utils/parkings.tsx index 7946474..6596fd4 100644 --- a/src/utils/parkings.tsx +++ b/src/utils/parkings.tsx @@ -2,7 +2,6 @@ import React from "react"; import { Session } from "next-auth"; import { reverseGeocode } from "~/utils/nomatim"; - import { type fietsenstallingen, } from "@prisma/client"; @@ -72,60 +71,6 @@ export const getAllFietstypen = async (): Promise => { } }; -const getOpenTimeKey = (day: DayPrefix): keyof ParkingDetailsType => { - return ('Open_' + day) as keyof ParkingDetailsType; -} - -const getDichtTimeKey = (day: DayPrefix): keyof ParkingDetailsType => { - return ('Dicht_' + day) as keyof ParkingDetailsType; -} - -export const formatOpeningTimes = ( - parkingdata: ParkingDetailsType, - dayidx: number, - day: DayPrefix, - label: string, - isNS: boolean = false -): React.ReactNode => { - const wkday = new Date().getDay(); - - const tmpopen: Date = new Date(parkingdata[getOpenTimeKey(day)]); - const hoursopen = tmpopen.getHours() - 1;//TODO - const minutesopen = String(tmpopen.getMinutes()).padStart(2, "0"); - - const tmpclose: Date = new Date(parkingdata[getDichtTimeKey(day)]); - const hoursclose = tmpclose.getHours() - 1;//TODO - const minutesclose = String(tmpclose.getMinutes()).padStart(2, "0"); - - let value = `${hoursopen}:${minutesopen} - ${hoursclose}:${minutesclose}`; - - let diff = Math.abs((tmpclose.getTime() - tmpopen.getTime()) / 1000); - - // 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/components/ParkingFacilityBlock.tsx - if (hoursopen === 1 && hoursclose === 1 && diff === 0) { - if (isNS) { - value = '24h'; - } else { - value = 'gesloten'; - } - } - else if (diff >= 86340) { - value = '24h' - } - else if (diff === 0) { - value = 'gesloten' - } - - return ( - <> -
{label}
-
{value}
- - ); -}; - const generateRandomChar = () => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; return chars[Math.floor(Math.random() * chars.length)]; diff --git a/src/utils/prisma.ts b/src/utils/prisma.ts index d314ca1..63b847c 100644 --- a/src/utils/prisma.ts +++ b/src/utils/prisma.ts @@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client"; import { prisma } from "~/server/db"; import { Session } from "next-auth"; -const getParkingsFromDatabase = async (sites: any, session: Session | null) => { +const getParkingsFromDatabase = async (sites: any, session: Session | null = null) => { let fietsenstallingen; diff --git a/src/utils/reports/baddata.tsx b/src/utils/reports/baddata.tsx new file mode 100644 index 0000000..0dc0c3b --- /dev/null +++ b/src/utils/reports/baddata.tsx @@ -0,0 +1,146 @@ +import type { fietsenstallingen, contacts } from "@prisma/client"; +import { ReportContent } from "./types"; +import { ParkingDetailsType } from "~/types"; +import { formatOpeningTimes, formatOpeningToday, createVeilistallenOrgLink } from "~/utils/parkings-openclose"; + +export const createFixBadDataReport = (fietsenstallingen: fietsenstallingen[], contacts: contacts[], showData: boolean = true): ReportContent => { + // Alles op een rij: + // 1. fietsenstallingen.stallingsID: not null & unique + // 2. fietsenstallingen.siteID: not null & foreign key naar contacts.id (is al geimplementeerd) + // 3. fietsenstallingen.exploitantID: foreign key naar contacts.id (is al geimplementeerd) + + const alwaysvisibleColumns = [ + "Title", + "Plaats", + "Type", + "OK", + "CheckStallingsID", + "CheckSiteID", + "CheckExploitantID", + + ]; + + const allColumns = [ + ...alwaysvisibleColumns, + "ID", + "StallingsID", + "SiteID", + "ExploitantID", + ]; + + const hiddenColumns = showData ? [] : allColumns.filter(col => !alwaysvisibleColumns.includes(col)); + + const report: ReportContent = { + title: 'Test for bad data', + data: { + columns: allColumns, + records: [], + hidden: hiddenColumns, + actions: [ + { + name: "Original", + action: async (data) => { + const stalling = fietsenstallingen.find((fs) => fs.ID === data.ID) as any as ParkingDetailsType; + const url = await createVeilistallenOrgLink(stalling); + window.open(url, '_blank'); + }, + icon: ( + + ) + }, + { + name: "Edit", + action: async (data) => { + const stalling = fietsenstallingen.find((fs) => fs.ID === data.ID) as any as ParkingDetailsType; + const url = `/?stallingid=${stalling.ID}`; + window.open(url, '_blank'); + }, + icon: ( + + ) + }, + ] + }, + }; + + // make StallingID count map + const countMap = new Map(); + + // Count occurrences of each StallingsID + for (const stalling of fietsenstallingen) { + if (stalling.StallingsID !== null) { + const count = countMap.get(stalling.StallingsID) || 0; + countMap.set(stalling.StallingsID, count + 1); + } + } + + fietsenstallingen.forEach((fietsenstalling: fietsenstallingen) => { + const parkingdata = fietsenstalling as any as ParkingDetailsType; + + let checkStallingsID = null; // ok + if (fietsenstalling.StallingsID === null) { + checkStallingsID = NULL + } else { + const count = countMap.get(fietsenstalling.StallingsID) || 0; + if (count > 1) { + checkStallingsID = DUPLICATE + } + }; + + let checkSiteID = null; // ok + if (fietsenstalling.SiteID === null) { + checkSiteID = NULL + } else { + const thecontact = contacts.find((contact) => contact.ID === fietsenstalling.SiteID); + if (!thecontact) { + checkSiteID = NOT FOUND + } + } + + + let checkExploitantID = null; // ok + if (fietsenstalling.ExploitantID !== null) { + const thecontact = contacts.find((contact) => contact.ID === fietsenstalling.ExploitantID); + if (!thecontact) { + checkExploitantID = NOT FOUND + } + } + + let fullCheck = (checkStallingsID === null && checkSiteID === null && checkExploitantID === null); + + report.data.records.push({ + "ID": parkingdata.ID, + "Title": parkingdata.Title, + "Plaats": parkingdata.Plaats, + "Type": parkingdata.Type, + "StallingsID": parkingdata.StallingsID, + "SiteID": parkingdata.SiteID, + "OK": fullCheck === false ? ERROR : null, + "ExploitantID": parkingdata.ExploitantID, + "CheckStallingsID": checkStallingsID, + "CheckSiteID": checkSiteID, + "CheckExploitantID": checkExploitantID, + + }); + }); + + return report; +} diff --git a/src/utils/reports/openingtimes.tsx b/src/utils/reports/openingtimes.tsx new file mode 100644 index 0000000..b3eb8d1 --- /dev/null +++ b/src/utils/reports/openingtimes.tsx @@ -0,0 +1,144 @@ +import type { fietsenstallingen } from "@prisma/client"; +import { ReportContent } from "./types"; +import { ParkingDetailsType } from "~/types"; +import moment from "moment"; +import { formatOpeningTimes, formatOpeningToday, createVeilistallenOrgLink } from "~/utils/parkings-openclose"; + +export const createOpeningTimesReport = (fietsenstallingen: fietsenstallingen[], timestamp: moment.Moment, showData: boolean): ReportContent => { + const alwaysvisibleColumns = [ + "Title", + "Plaats", + "Type", + "isNs", + "txt_ma", + "txt_di", + "txt_wo", + "txt_do", + "txt_vr", + "txt_za", + "txt_zo", + "txt_today" + ]; + + const allColumns = [ + ...alwaysvisibleColumns, + "ID", + "Open_ma", + "Dicht_ma", + "Open_di", + "Dicht_di", + "Open_wo", + "Dicht_wo", + "Open_do", + "Dicht_do", + "Open_vr", + "Dicht_vr", + "Open_za", + "Dicht_za", + "Open_zo", + "Dicht_zo", + "Openingstijden", + "EditorCreated" + ]; + + const hiddenColumns = showData ? [] : allColumns.filter(col => !alwaysvisibleColumns.includes(col)); + + const report: ReportContent = { + title: 'Opening/Closing times', + data: { + columns: allColumns, + records: [], + hidden: hiddenColumns, + actions: [ + { + name: "Original", + action: async (data) => { + const stalling = fietsenstallingen.find((fs) => fs.ID === data.ID) as any as ParkingDetailsType; + const url = await createVeilistallenOrgLink(stalling); + window.open(url, '_blank'); + }, + icon: ( + + ) + }, + { + name: "Edit", + action: async (data) => { + const stalling = fietsenstallingen.find((fs) => fs.ID === data.ID) as any as ParkingDetailsType; + const url = `/?stallingid=${stalling.ID}`; + window.open(url, '_blank'); + }, + icon: ( + + ) + }, + ] + }, + }; + + const formatUtcTime = (dbtime: Date | null) => { + if (null === dbtime) { + return "null" + } else { + return moment.utc(dbtime).format('HH:mm'); + } + } + + fietsenstallingen.forEach((fietsenstalling: fietsenstallingen) => { + const parkingdata = fietsenstalling as any as ParkingDetailsType; + const isNS = parkingdata.EditorCreated === "NS-connector" + const wkday = timestamp.day(); + + report.data.records.push({ + "ID": parkingdata.ID, + "Title": parkingdata.Title, + "Plaats": parkingdata.Plaats, + "Type": parkingdata.Type, + "isNs": isNS ? "NS" : "", + "txt_ma": formatOpeningTimes(parkingdata, "ma", "Maandag", wkday === 1, isNS), + "txt_di": formatOpeningTimes(parkingdata, "di", "Dinsdag", wkday === 2, isNS), + "txt_wo": formatOpeningTimes(parkingdata, "wo", "Woensdag", wkday === 3, isNS), + "txt_do": formatOpeningTimes(parkingdata, "do", "Donderdag", wkday === 4, isNS), + "txt_vr": formatOpeningTimes(parkingdata, "vr", "Vrijdag", wkday === 5, isNS), + "txt_za": formatOpeningTimes(parkingdata, "za", "Zaterdag", wkday === 6, isNS), + "txt_zo": formatOpeningTimes(parkingdata, "zo", "Zondag", wkday === 0, isNS), + "txt_today": formatOpeningToday(parkingdata, timestamp).message, + "Open_ma": formatUtcTime(parkingdata.Open_ma), + "Dicht_ma": formatUtcTime(parkingdata.Dicht_ma), + "Open_di": formatUtcTime(parkingdata.Open_di), + "Dicht_di": formatUtcTime(parkingdata.Dicht_di), + "Open_wo": formatUtcTime(parkingdata.Open_wo), + "Dicht_wo": formatUtcTime(parkingdata.Dicht_wo), + "Open_do": formatUtcTime(parkingdata.Open_do), + "Dicht_do": formatUtcTime(parkingdata.Dicht_do), + "Open_vr": formatUtcTime(parkingdata.Open_vr), + "Dicht_vr": formatUtcTime(parkingdata.Dicht_vr), + "Open_za": formatUtcTime(parkingdata.Open_za), + "Dicht_za": formatUtcTime(parkingdata.Dicht_za), + "Open_zo": formatUtcTime(parkingdata.Open_zo), + "Dicht_zo": formatUtcTime(parkingdata.Dicht_zo), + "Openingstijden": parkingdata.Openingstijden, + "EditorCreated": parkingdata.EditorCreated, + }); + }); + + return report; +} diff --git a/src/utils/reports/report-table.tsx b/src/utils/reports/report-table.tsx new file mode 100644 index 0000000..3fbe693 --- /dev/null +++ b/src/utils/reports/report-table.tsx @@ -0,0 +1,87 @@ +import React, { useMemo } from 'react'; +import { ReportContent } from './types'; +import { useTable, Column } from 'react-table'; + +const ReportTable: React.FC<{ reportContent: ReportContent }> = ({ reportContent }) => { + const data = useMemo(() => reportContent.data.records, [reportContent]); + + const columns = useMemo(() => { + const actionColumn: Column> = { + Header: 'Actions', + accessor: 'actions', + Cell: ({ row }) => ( +
+ {reportContent.data.actions?.map((action, index) => ( + + ))} +
+ ), + }; + + const hiddenColumns = reportContent.data.hidden || []; + + const dataColumns = reportContent.data.columns + .filter((col) => !hiddenColumns.includes(col)) + .map((col) => ({ + Header: col, + accessor: col, + })); + + return [actionColumn, ...dataColumns]; + }, [reportContent]); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + } = useTable({ columns, data }); + + return ( +
+

{reportContent.title}

+ + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + + ))} + + ))} + + + {rows.map((row) => { + prepareRow(row); + return ( + + {row.cells.map((cell) => ( + + ))} + + ); + })} + +
+ {column.render('Header')} +
+ {cell.render('Cell')} +
+
+ ); +}; + +export default ReportTable; diff --git a/src/utils/reports/types.ts b/src/utils/reports/types.ts new file mode 100644 index 0000000..7d40eb8 --- /dev/null +++ b/src/utils/reports/types.ts @@ -0,0 +1,18 @@ +export interface ReportContent { + title: string; + data: { + columns: string[]; + records: Record[]; + hidden?: string[]; + actions?: { + name: string; + icon: React.ReactNode | undefined; + action: (data: Record) => void; + }[]; + }; +} + +export const noReport: ReportContent = { + title: "No report", + data: { columns: [], records: [], actions: [], hidden: [] } +}