diff --git a/.env b/.env index f86855c..614d1b9 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ # HOST=localhost # PORT=3003 # NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003 - +ENV_ENV='.env' # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 @@ -10,6 +10,7 @@ NODE_ENV=development # mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma DATABASE=mysql://cart:cartpw@localhost:3306/cart # DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev +NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003 # // owner: dobromir.popov@gmail.com | Специално Свидетелстване София # // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 @@ -69,5 +70,6 @@ MAILTRAP_PASS=c7bc05f171c96c TELEGRAM_BOT=false TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM -NEXT_PUBLIC_VAPID_PUBLIC_KEY=BGxXJ0jdsQ4ihE7zp8mxrBO-QPSjeEtO9aCtPoMTuxc1VLW0OfRIt-DYinK9ekjTl2w-j0eQbeprIyBCpmmfciI -VAPID_PRIVATE_KEY=VXHu2NgcyM4J4w3O4grkS_0yLwWHCvVKDJexyBjqgx0 +WEB_PUSH_EMAIL=mwitnessing@gmail.com +NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=BGxXJ0jdsQ4ihE7zp8mxrBO-QPSjeEtO9aCtPoMTuxc1VLW0OfRIt-DYinK9ekjTl2w-j0eQbeprIyBCpmmfciI +WEB_PUSH_PRIVATE_KEY=VXHu2NgcyM4J4w3O4grkS_0yLwWHCvVKDJexyBjqgx0 diff --git a/.env.development b/.env.development index 6e49486..39f5519 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,6 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 # NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +ENV_ENV=.env.development PROTOCOL=https PORT=3003 HOST=localhost diff --git a/.env.development.devserver b/.env.development.devserver new file mode 100644 index 0000000..1a25f6e --- /dev/null +++ b/.env.development.devserver @@ -0,0 +1,13 @@ +# .ENV for vscode server .11 dev server # + +NODE_TLS_REJECT_UNAUTHORIZED=0 +# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +NODE_ENV=development + +PROTOCOL=http +PORT=3003 +HOST=cart.d-popov.com +NEXT_PUBLIC_PUBLIC_URL=https://cart.d-popov.com +DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev + +EMAIL_SENDER='"ССОМ [ТЕСТ] " ' diff --git a/.env.development.popov b/.env.development.popov index b6855cf..55494bb 100644 --- a/.env.development.popov +++ b/.env.development.popov @@ -1,3 +1,5 @@ +# .ENV for vscode server .11 dev server # + NODE_TLS_REJECT_UNAUTHORIZED=0 # NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert NODE_ENV=development diff --git a/.env.test b/.env.test index 38c9d2f..e76c4eb 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,6 @@ +# trying to run .env.test.staging did not work... falling back to .env.test NODE_ENV=test +ENV_ENV=test PROTOCOL=http HOST=staging.mwitnessing.com @@ -6,19 +8,4 @@ PORT= NEXT_PUBLIC_PUBLIC_URL=https://staging.mwitnessing.com # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 -NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 -# ? do we need to duplicate this? already defined in the deoployment yml file -DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo - -AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK -AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x -AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com - -# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com -# GOOGLE_SECRET=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 - -# EMAIL_SERVICE=mailtrap -# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io -# MAILTRAP_HOST=live.smtp.mailtrap.io -# MAILTRAP_USER=api -# MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d \ No newline at end of file +NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 \ No newline at end of file diff --git a/.env.test.staging b/.env.test.staging new file mode 100644 index 0000000..cf2ae59 --- /dev/null +++ b/.env.test.staging @@ -0,0 +1,26 @@ +# trying to run .env.test.staging did not work... falling back to .env.test +NODE_ENV=test +ENV_ENV=test.staging + +PROTOCOL=http +HOST=staging.mwitnessing.com +PORT= +NEXT_PUBLIC_PUBLIC_URL=https://staging.mwitnessing.com + +# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 +NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 +# ? do we need to duplicate this? already defined in the deoployment yml file +DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo + +AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK +AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x +AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com + +# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com +# GOOGLE_SECRET=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 + +# EMAIL_SERVICE=mailtrap +# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io +# MAILTRAP_HOST=live.smtp.mailtrap.io +# MAILTRAP_USER=api +# MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9df2aeb..b624557 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,6 @@ next-cart-app.zip !public/uploads/thumb/ certificates content/output/* -baseUrl.txt public/content/output/* public/content/output/shifts 2024.1.json +!public/content/uploads/* diff --git a/.vscode/launch.json b/.vscode/launch.json index b0e8405..ac9398c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -41,7 +41,7 @@ "type": "node-terminal" }, { - "name": "Run conda nodemon (DEV)", + "name": "Conda debug (DB)", "request": "launch", "type": "node-terminal", "cwd": "${workspaceFolder}", @@ -50,6 +50,16 @@ "APP_ENV": "development.popov" } }, + { + "name": "Conda run (DB)", + "request": "launch", + "type": "node-terminal", + "cwd": "${workspaceFolder}", + "command": "conda activate node && npm run start-env", + "env": { + "APP_ENV": "development.devserver" + } + }, { "name": "Run conda npm TEST", "request": "launch", diff --git a/_deploy/deoloy.azure.demo.yml b/_deploy/deoloy.azure.staging.yml similarity index 83% rename from _deploy/deoloy.azure.demo.yml rename to _deploy/deoloy.azure.staging.yml index 44a440c..d7a5af1 100644 --- a/_deploy/deoloy.azure.demo.yml +++ b/_deploy/deoloy.azure.staging.yml @@ -1,10 +1,11 @@ version: "3" services: - nextjs-app: # https://sofia.mwitnessing.com/ + nextjs-app: # https://sofia.mwhitnessing.com/ hostname: jwpw-app-staging # jwpw-nextjs-app-1 image: docker.d-popov.com/jwpw:latest volumes: - /mnt/docker_volumes/pw-demo/app/public/content/uploads/:/app/public/content/uploads + - /mnt/docker_volumes/pw-demo/app/logs:/app/logs environment: - APP_ENV=test - NODE_ENV=test @@ -14,12 +15,13 @@ services: - GIT_BRANCH=main - GIT_USERNAME=deploy - GIT_PASSWORD=L3Kr2R438u4F7 - command: sh -c " cd /app && npm install && npx next build && npm run nodeenv; tail -f /dev/null" + command: sh -c " cd /app && npm install && npx next build && npm run start-env; tail -f /dev/null" tty: true stdin_open: true restart: always networks: - infrastructure_default + - default mariadb: deploy: replicas: 1 @@ -33,6 +35,11 @@ services: MYSQL_DATABASE: jwpwsofia_demo MYSQL_USER: jwpwsofia_demo MYSQL_PASSWORD: dwxhns9p9vp248 + adminer: + image: adminer + restart: always + ports: + - 5002:8080 networks: infrastructure_default: external: true diff --git a/_deploy/maintenance.html b/_deploy/maintenance.html new file mode 100644 index 0000000..2e1c1c4 --- /dev/null +++ b/_deploy/maintenance.html @@ -0,0 +1,35 @@ + + + + + + + Поддръжка на сайта + + + +
+

Поддръжка и обновления!

+

Съжаляваме за неудобството, но в момента извършваме поддръжка на сайта. Ще се върнем онлайн скоро!

+

— Екипът на ССОМ: Специално Свидетелстване на Обществени Места - София

+
+ + diff --git a/_deploy/maintenance.yml b/_deploy/maintenance.yml index a5b2ff5..1ec55e6 100644 --- a/_deploy/maintenance.yml +++ b/_deploy/maintenance.yml @@ -6,7 +6,7 @@ services: volumes: - /mnt/docker_volumes/maintenance:/usr/share/nginx/html ports: - - "81:80" + - "3010:80" environment: - NGINX_HOST=nginx - NGINX_PORT=80 diff --git a/_doc/notes.mb b/_doc/notes.mb index a94440c..a1e8027 100644 --- a/_doc/notes.mb +++ b/_doc/notes.mb @@ -135,6 +135,12 @@ npx prisma migrate resolve --applied 20240201214719_assignment_add_repeat_freque npx prisma migrate dev --schema "mysql://cart:cart2023@192.168.0.10:3306/cart_dev" # -- does not work +## ---------------------- import database --------------------------------- ## +gunzip < /prisma/backups/jwpwsofia-20240430-bak.gz | mysql -u mysql_username -p database_name + +#export + + diff --git a/components/PwaManager.tsx b/components/PwaManager.tsx index 3bce4fc..c7787c8 100644 --- a/components/PwaManager.tsx +++ b/components/PwaManager.tsx @@ -1,19 +1,41 @@ import React, { useEffect, useState } from 'react'; import common from '../src/helpers/common'; // Ensure this path is correct +//use session to get user role +import { useSession } from "next-auth/react" +import e from 'express'; +import ProtectedRoute from './protectedRoute'; +import { UserRole } from '@prisma/client'; function PwaManager() { + //ToDo: for iOS, try to use apn? https://github.com/node-apn/node-apn/blob/master/doc/apn.markdown + const isSupported = () => + 'Notification' in window && + 'serviceWorker' in navigator && + 'PushManager' in window + + const [inProgress, setInProgress] = useState(false) const [deferredPrompt, setDeferredPrompt] = useState(null); const [isPWAInstalled, setIsPWAInstalled] = useState(false); const [isStandAlone, setIsStandAlone] = useState(false); const [isSubscribed, setIsSubscribed] = useState(false); const [subscription, setSubscription] = useState(null); const [registration, setRegistration] = useState(null); - const [notificationPermission, setNotificationPermission] = useState(Notification.permission); + const [notificationPermission, setNotificationPermission] = useState(isSupported() && Notification.permission); + const [subs, setSubs] = useState("") + + const { data: session } = useSession(); + // let isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN); + let isAdmin = false; + if (session) { + isAdmin = session.user.role === UserRole.ADMIN; + } + // Handle PWA installation useEffect(() => { - - setNotificationPermission(Notification.permission); + if (isSupported()) { + setNotificationPermission(Notification.permission); + } // Handle Push Notification Subscription if ('serviceWorker' in navigator && 'PushManager' in window) { @@ -28,9 +50,13 @@ function PwaManager() { }); } - - // Check if the app is running in standalone mode + // const isStandalone = window.matchMedia('(display-mode: standalone)').matches; + // if (isStandalone) { + // console.log('Running in standalone mode'); + // setIsPWAInstalled(true); + // } + if (window.matchMedia('(display-mode: standalone)').matches) { setIsStandAlone(true); } @@ -53,48 +79,80 @@ function PwaManager() { }; }, []); - const installPWA = async (e) => { - e.preventDefault(); + const installPWA = async (e) => { + console.log('Attempting to install PWA'); + e.preventDefault(); // Prevent default button action if (deferredPrompt) { + console.log('Prompting install'); deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; + console.log('Installation outcome:', outcome); if (outcome === 'accepted') { + console.log('User accepted the A2HS prompt'); setIsPWAInstalled(true); + } else { + console.log('User dismissed the A2HS prompt'); } - setDeferredPrompt(null); + setDeferredPrompt(null); // Clear the deferred prompt to manage its lifecycle + } else { + console.log('No deferred prompt available'); } }; - // Utility function for converting base64 string to Uint8Array - const base64ToUint8Array = base64 => { - const padding = '='.repeat((4 - (base64.length % 4)) % 4); - const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/'); - const rawData = window.atob(b64); - const outputArray = new Uint8Array(rawData.length); - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; - }; const subscribeToNotifications = async (e) => { try { e.preventDefault(); + if (!navigator.serviceWorker) { + console.error('Service worker is not supported by this browser.'); + return; + } + + const registration = await navigator.serviceWorker.ready; if (!registration) { console.error('Service worker registration not found.'); - registration return; } + + let vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; + if (!vapidPublicKey) { + // Fetch the public key from the server if not present in env variables + const response = await fetch('/api/notify', { method: 'GET' }); + const responseData = await response.json(); + vapidPublicKey = responseData.pk; + setSubs(responseData.subs); + if (!vapidPublicKey) { + throw new Error("Failed to fetch VAPID public key from server."); + } + } const sub = await registration.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: base64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY) + applicationServerKey: common.base64ToUint8Array(vapidPublicKey) }); // Call your API to save subscription data on server - setSubscription(sub); - setIsSubscribed(true); - console.log('Web push subscribed!'); + if (session.user?.id != null) { + await fetch(`/api/notify`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ subscription: sub, id: session.user.id }) + }).then(async response => { + if (!response.ok) { + throw new Error('Failed to save subscription data on server.'); + } + else { + console.log('Subscription data saved on server.'); + const s = await response.json(); + setSubs(s.subs); + setSubscription(sub); + setIsSubscribed(true); + console.log('Web push subscribed!'); + } + }); + } console.log(sub); } catch (error) { console.error('Error subscribing to notifications:', error); @@ -105,11 +163,34 @@ function PwaManager() { try { e.preventDefault(); - await subscription.unsubscribe(); - // Call your API to delete or invalidate subscription data on server - setSubscription(null); - setIsSubscribed(false); - console.log('Web push unsubscribed!'); + if (subscription) { + await subscription.unsubscribe(); + // Call your API to delete or invalidate subscription data on server + setSubscription(null); + setIsSubscribed(false); + if (session?.user?.id != null) { + await fetch(`/api/notify`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + //send the current subscription to be removed + body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint }) + } + ).then(async (response) => { + if (!response.ok) { + throw new Error('Failed to delete subscription data on server.'); + } + else { + console.log('Subscription data deleted on server.'); + const s = await response.json(); + setSubs(s.subs); + } + }); + } + console.log('Web push unsubscribed!'); + } } catch (error) { console.error('Error unsubscribing from notifications:', error); } @@ -118,14 +199,19 @@ function PwaManager() { // Function to request push notification permission const requestNotificationPermission = async (e) => { e.preventDefault(); - const permission = await Notification.requestPermission(); - setNotificationPermission(permission); - if (permission === "granted") { - // User granted permission - subscribeToNotifications(null); // Pass the required argument here - } else { - // User denied or dismissed permission - console.log("Push notifications permission denied."); + if (isSupported()) { + const permission = await Notification.requestPermission(); + setNotificationPermission(permission); + if (permission === "granted") { + // User granted permission + subscribeToNotifications(null); // Pass the required argument here + } else { + // User denied or dismissed permission + console.log("Push notifications permission denied."); + } + } + else { + console.error('Web push not supported'); } }; @@ -160,49 +246,132 @@ function PwaManager() { }); }; - return ( - <> + async function sendTestReminder(event: MouseEvent): Promise { + event.preventDefault(); + if (!subscription) { + console.error('Web push not subscribed'); + return; + } + + await fetch('/api/notify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ broadcast: true, message: 'Моля, въведете вашите предпочитания за юни до 25-ти май.' }) + }); + } + + async function sendTestCoverMe(event: MouseEvent): Promise { + event.preventDefault(); + if (!subscription) { + console.error('Web push not subscribed'); + return; + } + + await fetch('/api/notify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + broadcast: true, message: "Брат ТЕСТ търси заместник за 24-ти май от 10:00 ч. Можеш ли да го покриеш?", + //use fontawesome icons for actions + actions: [{ action: 'covermeaccepted', title: 'Да ', icon: '✅' }] + }) + }); + } + + async function deleteAllSubscriptions(event: MouseEvent): Promise { + event.preventDefault(); + await fetch(`/api/notify`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + //send the current subscription to be removed + body: JSON.stringify({ id: session.user.id }) + } + ).then(async response => { + if (!response.ok) { + throw new Error('Failed to delete subscription data on server.'); + } + else { + console.log('ALL subscriptions data deleted on server.'); + if (subscription) { + await subscription.unsubscribe(); + } + setSubs(""); + setSubscription(null); + setIsSubscribed(false); + } + }); + } + if (!isSupported()) { + return (
-

PWA Manager

- {!isStandAlone && !isPWAInstalled && ( - - )} - {isPWAInstalled &&

App is installed!

} - {isStandAlone &&

PWA App

} - - - - +

Това устройство не поддържа нотификации

-
- + ); + } + else { + return ( + <> +
+

{isAdmin && " PWA (admin)"}

+ {!isStandAlone && !isPWAInstalled && ( + + )} + {isPWAInstalled &&

Инсталирано!

} + {/* {isStandAlone &&

PWA App

} */} + + +
+ {isAdmin && +
+ + + +
+ } {notificationPermission !== "granted" && (
- - - - ); + {isAdmin && + } + + ); + } } - export default PwaManager; diff --git a/components/availability/AvailabilityForm.js b/components/availability/AvailabilityForm.js index 8c5936b..0197755 100644 --- a/components/availability/AvailabilityForm.js +++ b/components/availability/AvailabilityForm.js @@ -10,7 +10,10 @@ import { bgBG } from '../x-date-pickers/locales/bgBG'; import { ToastContainer } from 'react-toastify'; const common = require('src/helpers/common'); //todo import Availability type from prisma schema -import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; +import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; //ToDo obsolete +import { stat } from 'fs'; + +const { DateTime, FixedOffsetZone } = require('luxon'); @@ -19,7 +22,7 @@ const fetchConfig = async () => { return config.default; }; -export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, datePicker = false }) { +export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, cartEvent, datePicker = false }) { const router = useRouter(); const urls = { @@ -65,14 +68,16 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o }, []); - // Define the minimum and maximum times - const minTime = new Date(); - minTime.setHours(9, 0, 0, 0); // 8:00 AM - const maxTime = new Date(); - maxTime.setHours(19, 30, 0, 0); // 8:00 PM + // get cart event or set default time for Sofia timezone + // const minTime = cartEvent?.startTime || DateTime.now().set({ hour: 8, minute: 0, zone: 'Europe/Sofia' }).toJSDate(); + // const maxTime = cartEvent?.endTime || DateTime.now().set({ hour: 20, minute: 0, zone: 'Europe/Sofia' }).toJSDate(); + const d = DateTime.fromJSDate(day).setZone('Europe/Sofia', { keepLocalTime: true }); + const minTime = d.set({ hour: 9, minute: 0 }).toJSDate(); + const maxTime = d.set({ hour: 19, minute: 30 }).toJSDate(); useEffect(() => { - setTimeSlots(generateTimeSlots(minTime, maxTime, 90, availabilities)); + setTimeSlots(generateTimeSlots(new Date(minTime), new Date(maxTime), cartEvent?.shiftDuration || 90, availabilities)); + console.log("AvailabilityForm: minTime: " + common.getTimeFormatted(minTime) + ", maxTime: " + common.getTimeFormatted(maxTime), ", " + cartEvent?.shiftDuration || 90 + " min. shifts", cartEvent ? "cartEvent" : "cartEvent MISSING!!!"); }, []); @@ -187,15 +192,14 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o // Common function to set shared properties function setSharedAvailabilityProperties(availability, group, timeSlots) { - let startTime = new Date(availability.startTime || day); - startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0); - - let endTime = new Date(availability.endTime || day); - endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0); + const d = DateTime.fromJSDate(day).setZone('Europe/Sofia', { keepLocalTime: true }); + console.log("day: " + d.toISODate()); + let startTime = common.setTime(d, group[0].startTime).toJSDate(); + let endTime = common.setTime(d, group[group.length - 1].endTime).toJSDate(); availability.startTime = startTime; availability.endTime = endTime; - availability.name = common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime); + availability.name = common.getTimeFormatted(group[0].startTime) + "-" + common.getTimeFormatted(group[group.length - 1].endTime); availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport; availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport; @@ -209,7 +213,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o } else { availability.type = "OneTime" availability.repeatWeekly = false; - availability.dayOfMonth = startTime.getDate(); + availability.dayOfMonth = availability.startTime.getDate(); availability.endDate = null; } availability.isFromPreviousMonth = false; @@ -285,28 +289,17 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o const generateTimeSlots = (start, end, increment, items) => { const slots = []; let currentTime = start; - - const baseDate = new Date(Date.UTC(2000, 0, 1, 0, 0, 0)); - while (isBefore(currentTime, end)) { - let slotStart = normalizeTime(currentTime, baseDate); - let slotEnd = normalizeTime(addMinutes(currentTime, increment), baseDate); + let slotStart = currentTime; + let slotEnd = addMinutes(currentTime, increment); const isChecked = items.some(item => { - let itemStart = item.startTime ? normalizeTime(new Date(item.startTime), baseDate) : null; - let itemEnd = item.endTime ? normalizeTime(new Date(item.endTime), baseDate) : null; - - return itemStart && itemEnd && - (slotStart.getTime() < itemEnd.getTime()) && - (slotEnd.getTime() > itemStart.getTime()); - }); - - slots.push({ - startTime: slotStart, - endTime: slotEnd, - isChecked: isChecked, + return item.startTime && item.endTime && + common.isTimeBetween(item.startTime, item.endTime, slotStart) && + common.isTimeBetween(item.startTime, item.endTime, slotEnd); }); + slots.push({ startTime: slotStart, endTime: slotEnd, isChecked: isChecked, }); currentTime = addMinutes(currentTime, increment); } @@ -320,16 +313,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return slots; }; - // Normalize the time part of a date by using a base date - function normalizeTime(date, baseDate) { - return set(baseDate, { - hours: getHours(date), - minutes: getMinutes(date), - seconds: getSeconds(date), - milliseconds: 0 - }); - } - const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => { const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked)); const handleAllDayChange = (e) => { @@ -390,7 +373,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o {slots.map((slot, index) => { - const slotLabel = `${common.getTimeFomatted(slot.startTime)} до ${common.getTimeFomatted(slot.endTime)}`; + const slotLabel = `${common.getTimeFormatted(slot.startTime)} до ${common.getTimeFormatted(slot.endTime)}`; slot.transportNeeded = slot.isFirst || slot.isLast; // Determine if the current slot is the first or the last diff --git a/components/availability/AvailabilityFormDatePicker.js b/components/availability/AvailabilityFormDatePicker.js index 979975b..2a7d4ff 100644 --- a/components/availability/AvailabilityFormDatePicker.js +++ b/components/availability/AvailabilityFormDatePicker.js @@ -167,7 +167,7 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on if (!availability.name) { // availability.name = "От календара"; - availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime); + availability.name = common.getTimeFormatted(availability.startTime) + "-" + common.getTimeFormatted(availability.endTime); } availability.dayofweek = common.getDayOfWeekNameEnEnumForDate(availability.startTime); diff --git a/components/calendar/ShiftComponent.tsx b/components/calendar/ShiftComponent.tsx index 99e4976..7dd6341 100644 --- a/components/calendar/ShiftComponent.tsx +++ b/components/calendar/ShiftComponent.tsx @@ -173,6 +173,9 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions ass.canTransport = av.isWithTransportIn || av.isWithTransportOut; } + else { + borderStyles += 'border-l-4 border-red-500 '; + } if (publisherInfo.hasUpToDateAvailabilities) { //add green right border diff --git a/components/calendar/avcalendar.tsx b/components/calendar/avcalendar.tsx index 13acedd..d6497b5 100644 --- a/components/calendar/avcalendar.tsx +++ b/components/calendar/avcalendar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, use } from 'react'; import { Calendar, momentLocalizer, dateFnsLocalizer } from 'react-big-calendar'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import AvailabilityForm from '../availability/AvailabilityForm'; @@ -9,7 +9,7 @@ import common from '../../src/helpers/common'; import { toast } from 'react-toastify'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import moment from 'moment'; +import moment from 'moment'; // ToDo: obsolete, remove it import 'moment/locale/bg'; // Import Bulgarian locale import { ArrowLeftCircleIcon } from '@heroicons/react/24/outline'; @@ -18,11 +18,13 @@ import { MdToday } from 'react-icons/md'; import { useSwipeable } from 'react-swipeable'; import axiosInstance from '../../src/axiosSecure'; +import { set } from 'date-fns'; +import { get } from 'http'; // import { set, format, addDays } from 'date-fns'; // import { isEqual, isSameDay, getHours, getMinutes } from 'date-fns'; -import { filter } from 'jszip'; -import e from 'express'; +// import { filter } from 'jszip'; +// import e from 'express'; @@ -46,7 +48,7 @@ const messages = { // Any other labels you want to translate... }; -const AvCalendar = ({ publisherId, events, selectedDate }) => { +const AvCalendar = ({ publisherId, events, selectedDate, cartEvents }) => { const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN); @@ -65,7 +67,18 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { return { start, end }; }); + const [cartEvent, setCartEvent] = useState(null); + function getCartEvent(date) { + const dayOfWeek = common.getDayOfWeekNameEnEnumForDate(date); + const ce = cartEvents?.find(e => e.dayofweek === dayOfWeek); + return ce; + } + useEffect(() => { + //console.log("useEffect: ", date, selectedEvents, cartEvents); + setCartEvent(getCartEvent(date)); + }, + [date, selectedEvents]); // Update internal state when `events` prop changes useEffect(() => { @@ -113,6 +126,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { //setDisplayedEvents(evts); }, [visibleRange, evts, currentView]); + // todo: review that const handlers = useSwipeable({ onSwipedLeft: () => navigate('NEXT'), onSwipedRight: () => navigate('PREV'), @@ -201,18 +215,13 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { return existingEvents; }; - // Define min and max times - const minHour = 8; // 8:00 AM - const maxHour = 20; // 8:00 PM - const minTime = new Date(); - minTime.setHours(minHour, 0, 0); - const maxTime = new Date(); - maxTime.setHours(maxHour, 0, 0); - const totalHours = maxHour - minHour; + + // const totalHours = maxHour - minHour; const handleSelect = ({ mode, start, end }) => { - const startdate = typeof start === 'string' ? new Date(start) : start; - const enddate = typeof end === 'string' ? new Date(end) : end; + //we set the time to proper timezone + const startdate = common.setTimezone(start); + const enddate = common.setTimezone(end); if (!start || !end) return; //readonly for past dates (ToDo: if not admin) @@ -224,27 +233,15 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { end = common.setTimeHHmm(startdate, "23:59"); } - const startMinutes = common.getTimeInMinutes(start); - const endMinutes = common.getTimeInMinutes(end); + // Update date state and calculate events based on the new startdate + setDate(startdate); + const existingEvents = filterEvents(evts, publisherId, startdate); + console.log("handleSelect: ", existingEvents); - // Adjust start and end times to be within min and max hours - if (startMinutes < common.getTimeInMinutes(common.setTimeHHmm(start, minHour))) { - start = common.setTimeHHmm(start, minHour); - } - if (endMinutes > common.getTimeInMinutes(common.setTimeHHmm(end, maxHour))) { - end = common.setTimeHHmm(end, maxHour); - } - - setDate(start); - - // get exising events for the selected date - //ToDo: properly fix this. filterEvents does not return the expcted results - let existingEvents = filterEvents(evts, publisherId, startdate); - // if existingEvents is empty - create new with the selected range - // if (existingEvents.length === 0) { - // existingEvents = [{ startTime: start, endTime: end }]; - // } - console.log("handleSelect: " + existingEvents); + // Use the updated startdate for getCartEvent and ensure it reflects in the state properly + const cartEvent = getCartEvent(startdate); + setCartEvent(cartEvent); + console.log("cartEvent: ", cartEvent); setSelectedEvents(existingEvents); setIsModalOpen(true); }; @@ -353,15 +350,15 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { bgColor = event.isBySystem ? "bg-red-500" : (event.isConfirmed || true ? "bg-green-500" : "bg-yellow-500"); //event.title = event.publisher.name; //ToDo: add other publishers names - //event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime); + //event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime); } else { if (event.start !== undefined && event.end !== undefined && event.startTime !== null && event.endTime !== null) { try { if (event.type === "recurring") { - event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime); + event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime); } else { - event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime); + event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime); } } catch (err) { @@ -509,8 +506,8 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { onSelectSlot={handleSelect} onSelectEvent={handleEventClick} style={{ height: '100%', width: '100%' }} - min={minTime} // Set minimum time - max={maxTime} // Set maximum time + min={cartEvent?.startTime} // Set minimum time + max={cartEvent?.endTime} // Set maximum time messages={messages} view={currentView} views={['month', 'week', 'agenda']} @@ -530,6 +527,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { showAllEvents={true} onNavigate={setDate} className="rounded-lg shadow-lg" + longPressThreshold={150} // default value 250 /> {isModalOpen && (
@@ -540,6 +538,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { date={date} onDone={handleDialogClose} inline={true} + cartEvent={cartEvent} // Pass other props as needed />
diff --git a/components/cartevent/CartEventForm.tsx b/components/cartevent/CartEventForm.tsx index d6d6fdd..b8defce 100644 --- a/components/cartevent/CartEventForm.tsx +++ b/components/cartevent/CartEventForm.tsx @@ -69,8 +69,8 @@ export default function CartEventForm(props: IProps) { try { console.log("fetching cart event from component " + router.query.id); const { data } = await axiosInstance.get(urls.apiUrl + id); - data.startTime = common.formatTimeHHmm(data.startTime) - data.endTime = common.formatTimeHHmm(data.endTime) + data.startTime = common.getTimeFormatted(data.startTime) + data.endTime = common.getTimeFormatted(data.endTime) setEvt(data); console.log("id:" + evt.id); diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js index 1635270..e851008 100644 --- a/components/publisher/PublisherForm.js +++ b/components/publisher/PublisherForm.js @@ -246,15 +246,65 @@ export default function PublisherForm({ item, me }) { -
- {/* notifications */} -
- - - - - {/* prompt to install PWA */} -
+ + {/* notifications */} +
+
+ Известия + + {/* Email notifications group */} +
+

Известия по имейл

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* In-App notifications group */} +
+

Известия в приложението

+ +
+
{/* button to install PWA */} @@ -267,7 +317,7 @@ export default function PublisherForm({ item, me }) { {/* ADMINISTRATORS ONLY */}
- + {/* prompt to install PWA */}
handleToggleGroup('subscribedPublishers')} - /> - Абонирани: -
-
- {subscribedPublishers.map(pub => ( - {pub.name} - ))} -
- -
-
- +
+
+ handleToggleGroup('subscribedPublishers')} + /> + +
+
+ +
+
+ handleToggleGroup('availablePublishers')} + /> + +
+
+ +