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 && 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) => (
+
+ {column.render('Header')}
+ |
+ ))}
+
+ ))}
+
+
+ {rows.map((row) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell) => (
+
+ {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: [] }
+}