From 9f1e7db7059d8caead594daf42bfb3263cc0767f Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sun, 23 Jun 2024 23:00:12 +0300 Subject: [PATCH 1/4] setup UI dev env without conda (cherry picked from commit 7d36cf7179e317da1f9ccbb8a12687668fcc3e92) --- .vscode/launch.json | 11 +++++++++++ _doc/notes.mb | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index ed8ac12..8cce96d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -60,6 +60,17 @@ "APP_ENV": "development.devserver" } }, + { + "name": "!Run npm DEV (UI REDESIGN)", + "request": "launch", + "type": "node-terminal", + "cwd": "${workspaceFolder}", + "command": "npm run start-env", + "env": { + // "NODE_ENV": "test", + "APP_ENV": "development.devserver" + } + }, { "name": "Run conda npm TEST", "request": "launch", diff --git a/_doc/notes.mb b/_doc/notes.mb index 66700d6..aab4534 100644 --- a/_doc/notes.mb +++ b/_doc/notes.mb @@ -14,6 +14,15 @@ docker build -t dev-next-cart-app-img .devcontainer docker run -d -v /path/to/your/project:/workspace --name dev-next-cart-app dev-next-cart-app-img docker exec -it dev-next-cart-app /bin/bash +##### ----------- setup on new linux macine ----------- #### +sudo apt remove nodejs libnode-dev +sudo apt purge nodejs libnode-dev +sudo apt autoremove -y +# +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +apt install nodejs -y + + ##### ----------------- compose/deploy ----------------- ### # install docker if inside docker (vscode-server)# apt-get update && apt-get install -y docker.io # .10 > /mnt/apps/DEV/SSS/next-cart-app/next-cart-app/ From 260569e6ab928e213b59feeaa4766dd315f80985 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 25 Jun 2024 01:02:10 +0300 Subject: [PATCH 2/4] allow sending push to unanswered publishers; comment dangerous admin functions --- components/PwaManager.tsx | 107 +++++++++++++++++++------------ components/survey/SurveyForm.tsx | 43 +++++++++++++ pages/api/notify.ts | 23 ++++--- pages/cart/calendar/index.tsx | 29 +++++---- 4 files changed, 143 insertions(+), 59 deletions(-) diff --git a/components/PwaManager.tsx b/components/PwaManager.tsx index 5fe808f..464afda 100644 --- a/components/PwaManager.tsx +++ b/components/PwaManager.tsx @@ -35,6 +35,7 @@ function PwaManager({ subs }) { useEffect(() => { if (isSupported()) { setNotificationPermission(Notification.permission); + getSubscriptionCount(); } // Handle Push Notification Subscription @@ -77,6 +78,10 @@ function PwaManager({ subs }) { window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); window.removeEventListener('appinstalled', handleAppInstalled); }; + + + + }, []); @@ -127,6 +132,7 @@ function PwaManager({ subs }) { throw new Error("Failed to fetch VAPID public key from server."); } } + const sub = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: common.base64ToUint8Array(vapidPublicKey) @@ -197,6 +203,19 @@ function PwaManager({ subs }) { } }; + const getSubscriptionCount = async () => { + try { + const response = await fetch('/api/notify?id=' + session.user.id, { method: 'GET' }); + if (!response.ok) { + throw new Error('Failed to fetch subscription data.'); + } + const result = await response.json(); + setSubs(result.subs); + } catch (error) { + console.error('Error fetching subscription data:', error); + } + }; + // Function to request push notification permission const requestNotificationPermission = async (e) => { e.preventDefault(); @@ -243,48 +262,56 @@ function PwaManager({ subs }) { headers: { 'Content-Type': 'application/json' }, - //sends test notification to the current subscription - // body: JSON.stringify({ subscription }) - //sends test notification to all subscriptions of this user - body: JSON.stringify({ id: session.user.id, title: "Тестово уведомление", message: "Това е тестово уведомление" }) + body: JSON.stringify( + { + id: session.user.id, + title: "Тестово уведомление", + message: "Това е тестово уведомление", + actions: [{ action: 'test', title: 'Тест', icon: '✅' }, + { action: 'close', title: 'Затвори', icon: '❌' }] + }) }); }; - async function sendTestReminder(event: MouseEvent): Promise { - event.preventDefault(); - if (!subscription) { - console.error('Web push not subscribed'); - 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-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'" }) - }); - } + // await fetch('/api/notify', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json' + // }, + // body: JSON.stringify({ + // broadcast: true, + // message: "Мили братя, искаме да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'" + // }) + // }); + // } - async function sendTestCoverMe(event: MouseEvent): Promise { - event.preventDefault(); - if (!subscription) { - console.error('Web push not subscribed'); - return; - } + // 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: '✅' }] - }) - }); - } + // await fetch('/api/notify', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json' + // }, + // body: JSON.stringify({ + // id: session.user.id, + // message: "Брат ТЕСТ търси заместник за 24-ти май от 10:00 ч. Можеш ли да го покриеш?", + // //use fontawesome icons for actions + // actions: [{ action: 'covermeaccepted', title: 'Да ', icon: '✅' }] + // }) + // }); + // } async function deleteAllSubscriptions(event: MouseEvent): Promise { event.preventDefault(); @@ -358,22 +385,22 @@ function PwaManager({ subs }) { {isAdmin &&
- - + */}
} {notificationPermission !== "granted" && ( diff --git a/components/survey/SurveyForm.tsx b/components/survey/SurveyForm.tsx index 60c3ace..25f4373 100644 --- a/components/survey/SurveyForm.tsx +++ b/components/survey/SurveyForm.tsx @@ -203,6 +203,29 @@ const SurveyForm: React.FC = ({ existingItem }) => { (err) => alert('Не успяхме да копираме имената: ', err) ); }; + const sendIndividualNotification = async (id, message) => { + const response = await fetch('/api/notify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id, + title: 'Нямаме отговор', + message: `${message}`, + }) + }); + + if (response.ok) { + console.log(`Notification sent successfully to ${name}`); + } else { + console.error(`Failed to send notification to ${name}`); + } + }; + + const handleSendNotificationsToAllUnanswered = async (message) => { + getIdsForUnanswered().forEach((id, index) => sendIndividualNotification(id, message)); + }; return (
@@ -281,6 +304,26 @@ const SurveyForm: React.FC = ({ existingItem }) => { {item.messages ? item.messages.filter((message) => !message.answer).length : 0}
+ +
+ {getIdsForUnanswered().map((id) => { + const pub = pubs.find((p) => p.id === id); + const name = pub ? `${pub.firstName} ${pub.lastName}` : '???'; + return ( + + ); + })} +
)} diff --git a/pages/api/notify.ts b/pages/api/notify.ts index f554728..e97bde5 100644 --- a/pages/api/notify.ts +++ b/pages/api/notify.ts @@ -33,19 +33,21 @@ const Notification = async (req, res) => { select: { pushSubscription: true } }); subs = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription.length : (publisher.pushSubscription ? 1 : 0); + res.send({ subs }) res.end() return + } else { + // send the public key in the response headers + //res.setHeader('Content-Type', 'text/plain') + res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs }) + res.end() } - // send the public key in the response headers - //res.setHeader('Content-Type', 'text/plain') - res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs }) - res.end() } if (req.method == 'PUT') { // store the subscription object in the database // publisher.pushSubscription = subscription const prisma = common.getPrismaClient(); - const { subscription, id } = req.body + const { subscription, id, name } = req.body const publisher = await prisma.publisher.findUnique({ where: { id }, select: { pushSubscription: true } @@ -105,14 +107,19 @@ const Notification = async (req, res) => { if (req.method == 'POST') {//title = "ССС", message = "Ще получите уведомление по този начин.") - const { subscription, id, broadcast, title = 'ССОМ', message = 'Ще получавате уведомления така.', actions } = req.body + const { subscription, id, ids, broadcast, title = 'ССОМ', message = 'Ще получавате уведомления така.', actions } = req.body if (broadcast) { await broadcastPush(title, message, actions) res.statusCode = 200 res.end() return - } - else if (id) { + } else if (ids && ids.length) { + console.log('Sending push notifications to publishers ', ids); + await Promise.all(ids.map(_id => sendPush(_id, title, message, actions))); + res.statusCode = 200; + res.end(); + return; + } else if (id) { console.log('Sending push notification to publisher ', id) await sendPush(id, title, message, actions) res.statusCode = 200 diff --git a/pages/cart/calendar/index.tsx b/pages/cart/calendar/index.tsx index 4a04176..6148d6e 100644 --- a/pages/cart/calendar/index.tsx +++ b/pages/cart/calendar/index.tsx @@ -834,18 +834,25 @@ export default function CalendarPage({ initialEvents, initialShifts }) { 'Content-Type': 'application/json' }, body: JSON.stringify({ - id: pub.id, message: "Тестово съобщение", title: "Това е тестово съобщение от https://sofia.mwitnessing.com", actions: [ - { - action: 'open_url', - title: 'Open URL', - icon: '/images/open-url.png' - }, - { - action: 'dismiss', - title: 'Dismiss', - icon: '/images/dismiss.png' - } + id: pub.id, + message: "Тестово съобщение", + title: "Това е тестово съобщение от https://sofia.mwitnessing.com", + actions: [ + { action: 'OK', title: 'OK', icon: '✅' }, + { action: 'close', title: 'Затвори', icon: '❌' } ] + // actions: [ + // { + // title: 'Open URL', + // action: 'open_url', + // icon: '/images/open-url.png' + // }, + // { + // title: 'Dismiss', + // action: 'dismiss', + // icon: '/images/dismiss.png' + // } + // ] }) }) }} From 8279514e8a9c45745dd640e064b0b52936991035 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 25 Jun 2024 01:52:18 +0300 Subject: [PATCH 3/4] ErrorBoundry added --- components/ErrorBoundary.tsx | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 components/ErrorBoundary.tsx diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx new file mode 100644 index 0000000..4c62e98 --- /dev/null +++ b/components/ErrorBoundary.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +const logger = require('../src/logger'); + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error, info) { + // You can also log the error to an error reporting service + console.error(error, info); + logger.error(error, info); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return

Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.

; + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file From 1936a9cb78a25629d8e4f3b4a107d2ad42e87a60 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 25 Jun 2024 17:54:30 +0300 Subject: [PATCH 4/4] logging, error boundry --- components/ErrorBoundary.tsx | 16 ++++--- components/layout.tsx | 5 ++- pages/api/index.ts | 1 + pages/cart/publishers/stats.tsx | 14 +++++- src/helpers/data.js | 77 +++++++++++++++++---------------- src/logger.js | 18 ++++++-- 6 files changed, 83 insertions(+), 48 deletions(-) diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index 4c62e98..2dbbd89 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,10 +1,14 @@ import React from 'react'; -const logger = require('../src/logger'); class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; + this.logger = null; + + if (typeof window === 'undefined') { + this.logger = require('../src/logger'); + } } static getDerivedStateFromError(error) { @@ -13,14 +17,16 @@ class ErrorBoundary extends React.Component { } componentDidCatch(error, info) { - // You can also log the error to an error reporting service + // Log the error to an error reporting service console.error(error, info); - logger.error(error, info); + if (this.logger) { + this.logger.error(`${error}: ${info.componentStack}`); + } } render() { if (this.state.hasError) { - // You can render any custom fallback UI + // Render any custom fallback UI return

Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.

; } @@ -28,4 +34,4 @@ class ErrorBoundary extends React.Component { } } -export default ErrorBoundary; \ No newline at end of file +export default ErrorBoundary; diff --git a/components/layout.tsx b/components/layout.tsx index 77f1073..39d91c5 100644 --- a/components/layout.tsx +++ b/components/layout.tsx @@ -10,6 +10,7 @@ import Body from 'next/document' import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { set } from "date-fns" +import ErrorBoundary from "./ErrorBoundary"; export default function Layout({ children }) { const router = useRouter(); @@ -61,7 +62,9 @@ export default function Layout({ children }) {
- {children} + + {children} +
{/* Modal container */}
diff --git a/pages/api/index.ts b/pages/api/index.ts index f26e8cc..9b7a855 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -511,6 +511,7 @@ export async function getMonthlyStatistics(selectFields, filterDate) { export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true, includeOldAvailabilities = false) { return dataHelper.filterPublishersNew(selectFields, filterDate, isExactTime, isForTheMonth, false, isWithStats, includeOldAvailabilities); + // async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false, id = null, filterAvailabilitiesByDate = true) } // availabilites filter: diff --git a/pages/cart/publishers/stats.tsx b/pages/cart/publishers/stats.tsx index 0e10c77..6b9d615 100644 --- a/pages/cart/publishers/stats.tsx +++ b/pages/cart/publishers/stats.tsx @@ -324,10 +324,22 @@ export default ContactsPage; export const getServerSideProps = async (context) => { const allPublishers = await data.getAllPublishersWithStatisticsMonth(new Date()); - //merge first and last name + // Merge first and last name and serialize Date objects allPublishers.forEach(publisher => { publisher.name = `${publisher.firstName} ${publisher.lastName}`; + + if (publisher.currentMonthAvailability) { + publisher.currentMonthAvailability = publisher.currentMonthAvailability.map(availability => { + return { + ...availability, + startTime: availability.startTime instanceof Date ? availability.startTime.toISOString() : availability.startTime, + endTime: availability.endTime instanceof Date ? availability.endTime.toISOString() : availability.endTime, + dateOfEntry: availability.dateOfEntry instanceof Date ? availability.dateOfEntry.toISOString() : availability.dateOfEntry, + }; + }); + } }); + return { props: { allPublishers diff --git a/src/helpers/data.js b/src/helpers/data.js index 4713e0e..00aeeb5 100644 --- a/src/helpers/data.js +++ b/src/helpers/data.js @@ -390,43 +390,7 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false ///console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`); - // include repeating weekly availabilities. generate occurrences for the month - // convert matching weekly availabilities to availabilities for the day to make further processing easier on the client. - publishers.forEach(pub => { - pub.availabilities = pub.availabilities.map(avail => { - if (avail.dayOfMonth == null) { - if (filterAvailabilitiesByDate && !isForTheMonth) { - // filter out repeating availabilities when on other day of week - if (filterTimeFrom) { - if (avail.dayofweek != dayOfWeekEnum) { - return null; - } - } - } - let newStart = new Date(filterDate); - newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0); - let newEnd = new Date(filterDate); - newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0); - return { - ...avail, - startTime: newStart, - endTime: newEnd - } - } - else { - if (filterAvailabilitiesByDate && !isForTheMonth) { - if (avail.startTime >= filterTimeFrom && avail.startTime <= filterTimeTo) { - return avail; - } - return null; - } - return avail; - } - }) - .filter(avail => avail !== null); - }); - - + // ---------------------------------------------- statistics ---------------------------------------------- let currentWeekStart, currentWeekEnd; if (isWithStats) { @@ -494,8 +458,45 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false return avail.startTime >= filterDate && avail.startTime <= filterTimeTo; }); } - }); + + // ---------------------------------------------- + // include repeating weekly availabilities. generate occurrences for the month + // convert matching weekly availabilities to availabilities for the day to make further processing easier on the client. + publishers.forEach(pub => { + pub.availabilities = pub.availabilities.map(avail => { + if (avail.dayOfMonth == null) { + if (filterAvailabilitiesByDate && !isForTheMonth) { + // filter out repeating availabilities when on other day of week + if (filterTimeFrom) { + if (avail.dayofweek != dayOfWeekEnum) { + return null; + } + } + } + let newStart = new Date(filterDate); + newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0); + let newEnd = new Date(filterDate); + newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0); + return { + ...avail, + startTime: newStart, + endTime: newEnd + } + } + else { + if (filterAvailabilitiesByDate && !isForTheMonth) { + if (avail.startTime >= filterTimeFrom && avail.startTime <= filterTimeTo) { + return avail; + } + return null; + } + return avail; + } + }) + .filter(avail => avail !== null); + }); + // ToDo: test case/unit test // ToDo: check and validate the filtering and calculations if (isExactTime) { diff --git a/src/logger.js b/src/logger.js index 5cf11b9..5d951b0 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,11 +1,22 @@ const winston = require('winston'); require('winston-daily-rotate-file'); +const fs = require('fs'); +const path = require('path'); +// Define the logs directory path +const logDirectory = path.join(__dirname, '../logs'); + +// Ensure the logs directory exists +if (!fs.existsSync(logDirectory)) { + fs.mkdirSync(logDirectory); +} + +// Define the log configuration const logConfiguration = { - 'transports': [ + transports: [ new winston.transports.DailyRotateFile({ - filename: './logs/application-%DATE%.log', - datePattern: 'YYYY-MM-DD', // new file is created every hour: 'YYYY-MM-DD-HH' + filename: path.join(logDirectory, 'application-%DATE%.log'), + datePattern: 'YYYY-MM-DD', // new file is created every day zippedArchive: true, maxSize: '20m', maxFiles: '90d', @@ -20,6 +31,7 @@ const logConfiguration = { ) }; +// Create the logger const logger = winston.createLogger(logConfiguration); module.exports = logger;