diff --git a/.env b/.env index d0883ef..24dc32d 100644 --- a/.env +++ b/.env @@ -6,13 +6,8 @@ # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 +NODE_ENV=development # mysql -DATABASE_PROVIDER=mysql -# DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart_dev -DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev -# DATABASE_URL=mysql://cart:cartpw@20.101.62.76:3307/cart - -# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart # npx prisma migrate dev # // owner: dobromir.popov@gmail.com | Специално Свидетелстване София # // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 diff --git a/.env.development b/.env.development index 21fcc42..40806ac 100644 --- a/.env.development +++ b/.env.development @@ -4,9 +4,8 @@ PROTOCOL=https PORT=3003 HOST=localhost NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003 +# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev +DATABASE=mysql://cart:cartpw@localhost:3306/cart SSL_KEY=./certificates/localhost-key.pem SSL_CERT=./certificates/localhost.pem - -DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev -# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart \ No newline at end of file diff --git a/.env.development.raph b/.env.development.raph new file mode 100644 index 0000000..52d1a21 --- /dev/null +++ b/.env.development.raph @@ -0,0 +1,10 @@ +NODE_TLS_REJECT_UNAUTHORIZED=0 +# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +PROTOCOL=http +PORT=3003 +HOST=localhost +NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003 +DATABASE="mysql://root:mdp-11000@127.0.0.1:3306/cart?connection_limit=5&charset=utf8mb4&collation=utf8mb4_unicode_ci" + +SSL_KEY=./certificates/localhost-key.pem +SSL_CERT=./certificates/localhost.pem diff --git a/.env.production b/.env.production index 5853ac7..15d66db 100644 --- a/.env.production +++ b/.env.production @@ -6,4 +6,4 @@ NEXT_PUBLIC_PUBLIC_URL= https://sofia.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_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia \ No newline at end of file +DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia \ No newline at end of file diff --git a/.env.test b/.env.test index 2e62731..b99f907 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,5 @@ +NODE_ENV=test + PROTOCOL=http HOST=staging.mwitnessing.com PORT= @@ -6,7 +8,7 @@ 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_URL=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo +DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo APPLE_ID= APPLE_TEAM_ID= diff --git a/.vscode/launch.json b/.vscode/launch.json index 963bb15..a2cc8ff 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,23 @@ "version": "0.2.0", "configurations": [ { - "name": "Run npm nodemon (DEV)", + "name": "Run npm nodemon (DB)", "command": "npm run debug", "request": "launch", "type": "node-terminal", "preLaunchTask": "killInspector", "env": { - "NODE_ENV": "development" + "APP_ENV": "development" + } + }, + { + "name": "Run npm nodemon (Raph)", + "command": "npm run debug", + "request": "launch", + "type": "node-terminal", + "preLaunchTask": "killInspector", + "env": { + "APP_ENV": "development.raph" } }, { diff --git a/_deploy/demo.10.yml b/_deploy/demo.10.yml index 307260e..06a3a49 100644 --- a/_deploy/demo.10.yml +++ b/_deploy/demo.10.yml @@ -10,7 +10,7 @@ services: - /mnt/apps/docker_volumes/cart/app/next-cart-app:/app environment: - NODE_ENV=demo - - DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart + - DATABASE=mysql://cart:cart2023@192.168.0.10:3306/cart #command: sh -c "apk update && apk add git && rm -rf /tmp/clone && git clone https://git.d-popov.com/popov/next-cart-app.git /tmp/clone && rm -rf /app/* && cp -R /tmp/clone/next-cart-app/* /app/ && rm -rf /tmp/clone && npm cache clean --force && rm -rf /app/node_modules /app/package-lock.json && npm --silent --prefix /app install /app && npx --prefix /app prisma generate && npm --prefix /app run test; tail -f /dev/null" #command: sh -c "rm -rf /tmp/clone && git clone https://git.d-popov.com/popov/next-cart-app.git /tmp/clone && rm -rf /app/* && cp -R /tmp/clone/next-cart-app/* /app/ && rm -rf /tmp/clone && npm cache clean --force && rm -rf /app/node_modules /app/package-lock.json && npm --silent --prefix /app install /app && npx --prefix /app prisma generate && npm --prefix /app run test; tail -f /dev/null" command: sh -c "npm cache clean --force && rm -rf /app/node_modules /app/package-lock.json && npm --silent --prefix /app install /app && npx --prefix /app prisma generate && npm --prefix /app run test; tail -f /dev/null" diff --git a/_deploy/demo.11-demo.yml b/_deploy/demo.11-demo.yml index 07a4a7f..3a3a4d9 100644 --- a/_deploy/demo.11-demo.yml +++ b/_deploy/demo.11-demo.yml @@ -8,7 +8,7 @@ services: - /mnt/apps/DEV/cart-demo:/app environment: - NODE_ENV=demo - - DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart + - DATABASE=mysql://cart:cart2023@192.168.0.10:3306/cart command: sh -c " cd /app && npm run test; tail -f /dev/null" tty: true stdin_open: true diff --git a/_deploy/deoloy.azure.demo.yml b/_deploy/deoloy.azure.demo.yml index d81bbbb..8dac626 100644 --- a/_deploy/deoloy.azure.demo.yml +++ b/_deploy/deoloy.azure.demo.yml @@ -1,14 +1,15 @@ 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 environment: - - NODE_ENV=demo + - APP_ENV=test + - NODE_ENV=test - TZ=Europe/Sofia - - DATABASE_URL=mysql://jwpwsofia_demo:dwxhns9p9vp248@jwpwsofia:3306/jwpwsofia_demo + - DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb-demo:3306/jwpwsofia_demo - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git - GIT_BRANCH=main - GIT_USERNAME=deploy @@ -21,19 +22,17 @@ services: - infrastructure_default mariadb: deploy: - replicas: 0 + replicas: 1 hostname: mariadb-demo - image: mariadb:latest #mariadb:10.4 + image: mysql:latest #mariadb:10.4 volumes: - - /mnt/docker_volumes/pw-demo/data/mysql:/var/lib/mysql + - /mnt/docker_volumes/pw-demo2/data/mysql:/var/lib/mysql environment: MARIADB_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93 MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93 MYSQL_DATABASE: jwpwsofia_demo MYSQL_USER: jwpwsofia_demo MYSQL_PASSWORD: dwxhns9p9vp248 - networks: - - infrastructure_default networks: infrastructure_default: external: true diff --git a/_deploy/deoloy.azure.production.yml b/_deploy/deoloy.azure.production.yml index d653294..7f8bcdb 100644 --- a/_deploy/deoloy.azure.production.yml +++ b/_deploy/deoloy.azure.production.yml @@ -12,8 +12,8 @@ services: environment: - NODE_ENV=production - TZ=Europe/Sofia - - DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia - #- DATABASE_URL=postgres://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia + - DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia + #- DATABASE=postgres://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git - GIT_BRANCH=production - GIT_USERNAME=deploy diff --git a/_deploy/homelab.deploy.production.yml b/_deploy/homelab.deploy.production.yml index 402af4d..3c1826a 100644 --- a/_deploy/homelab.deploy.production.yml +++ b/_deploy/homelab.deploy.production.yml @@ -6,7 +6,7 @@ services: - "5001:3000" environment: - NODE_ENV=prod - - DATABASE_URL=mysql://cart:o74x642Rc8@mariadb:3306/cart + - DATABASE=mysql://cart:o74x642Rc8@mariadb:3306/cart - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git - GIT_USERNAME=deploy - GIT_PASSWORD=%L3Kr2R438u4F7^%40 diff --git a/_deploy/prod.Dockerfile b/_deploy/prod.Dockerfile index f894520..14d7092 100644 --- a/_deploy/prod.Dockerfile +++ b/_deploy/prod.Dockerfile @@ -4,6 +4,8 @@ FROM node:current-alpine # Set environment variables for Node.js ENV NODE_ENV=production +# ENV MYSQL_ROOT_PASSWORD=pass +ENV MYSQL_DATABASE=cart # Create and set the working directory WORKDIR /app diff --git a/_deploy/sample.docker-compose.yml b/_deploy/sample.docker-compose.yml index 06a9ae4..32347a9 100644 --- a/_deploy/sample.docker-compose.yml +++ b/_deploy/sample.docker-compose.yml @@ -40,7 +40,7 @@ services: - /mnt/data/apps/docker_volumes/cart/app:/app environment: - NODE_ENV=demo - - DATABASE_URL=mysql://cart:cartpw2024@mariadb:3306/cart + - DATABASE=mysql://cart:cartpw2024@mariadb:3306/cart #! entrypoint: ["/bin/sh", "/entrypoint.sh"] #run: npm install && npx prisma generate && npm run test; # command: "npx prisma migrate deploy && npx prisma migrate deploy && npm run build && npm run start" diff --git a/_doc/ToDo.md b/_doc/ToDo.md index bd32c11..b8801e8 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -196,3 +196,9 @@ fix Time ZONE (currently Z, but it leads to shift when the DST changes ( winter fix repeating availabilities - Tanq kolcjanova only blue first thursday add assignment in calendar planner fix database + +-- +emails +mobile apps +apple login +разрешителни - upload diff --git a/_doc/notes.mb b/_doc/notes.mb index 7b8ee37..a94440c 100644 --- a/_doc/notes.mb +++ b/_doc/notes.mb @@ -111,6 +111,11 @@ export OPENAI_API_KEY=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 # dev- # ----------------------------------------------update PRISMA schema/sync database ----------------------------------------------- # # prisma migrate dev --create-only +NODE_ENV=production npx prisma migrate deploy +#windows +$env:DATABASE="mysql://cart:cartpw@localhost:3306/cart"; npx prisma migrate deploy +$env:DATABASE="mysql://cart:cartpw@192.168.0.10:3306/cart_dev"; npx prisma migrate deploy + npx prisma generate npx prisma migrate dev --name fix_nextauth_schema --create-only >Prisma Migrate created the following migration without applying it 20231214163235_fix_nextauth_schema @@ -196,3 +201,8 @@ ncu -u enable apple ID: curl https://gist.githubusercontent.com/balazsorban44/09613175e7b37ec03f676dcefb7be5eb/raw/b0d31aa0c7f58e0088fdf59ec30cad1415a3475b/apple-gen-secret.mjs -o apple-gen-secret.mjs + + + + +Project setup: diff --git a/components/availability/AvailabilityForm.js b/components/availability/AvailabilityForm.js index 0c91f59..f8f52c6 100644 --- a/components/availability/AvailabilityForm.js +++ b/components/availability/AvailabilityForm.js @@ -25,6 +25,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o indexUrl: "/cart/availabilities" }; + //coalsce existingItems to empty array + existingItems = existingItems || []; + const [editMode, setEditMode] = useState(existingItems.length > 0); const [publisher, setPublisher] = useState({ id: publisherId }); const [day, setDay] = useState(new Date(date)); diff --git a/components/calendar/avcalendar.tsx b/components/calendar/avcalendar.tsx index 5403f1f..a83d257 100644 --- a/components/calendar/avcalendar.tsx +++ b/components/calendar/avcalendar.tsx @@ -57,13 +57,13 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { // Update internal state when `events` prop changes useEffect(() => { //if we have isBySystem - set type to assignment - let updatedEvents = events.map(event => { + let updatedEvents = events?.map(event => { if (event.isBySystem) { event.type = "assignment"; } return event; }); - updatedEvents = events.map(event => ({ + updatedEvents = events?.map(event => ({ ...event, date: new Date(event.startTime).setHours(0, 0, 0, 0), startTime: new Date(event.startTime), diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js index e3e6f16..99bde59 100644 --- a/components/publisher/PublisherForm.js +++ b/components/publisher/PublisherForm.js @@ -185,10 +185,13 @@ export default function PublisherForm({ item, me }) { -
- - + +
+
+ + +
@@ -232,60 +235,68 @@ export default function PublisherForm({ item, me }) { -
- - -
-
- - -
-
- - +
+
+ + +
+
+
+ + + +
- -
- - -
-
-
- - - - - - + + {/* ADMINISTRATORS ONLY */} + +
+
+ +
+
+ + +
+
+ + +
+
+
+ + + + + + +
+
+
+ + +
+ + Телеграм + Телеграм +
-
- - -
- - Телеграм - Телеграм -
{/* ---------------------------- Actions --------------------------------- */}
@@ -312,7 +323,7 @@ export default function PublisherForm({ item, me }) {
-
+ ) diff --git a/package-lock.json b/package-lock.json index f9dac5e..c876ac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pwwa", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pwwa", - "version": "1.1.1", + "version": "1.1.2", "dependencies": { "@auth/prisma-adapter": "^1.4.0", "@emotion/react": "^11.11.3", @@ -80,6 +80,7 @@ "tailwindcss": "^3.4.1", "tw-elements": "^1.1.0", "typescript": "^5", + "uuid": "^9.0.1", "webpack-bundle-analyzer": "^4.10.1", "winston": "^3.11.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz", diff --git a/package.json b/package.json index e8738af..370ffd0 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,15 @@ }, "homepage": "https://git.d-popov.com/popov/next-cart-app", "scripts": { - "debug": "nodemon --inspect server.js", - "debug-env-dev": "dotenv -e .env.development -- nodemon --inspect server.js", + "debug": "node server.js", + "debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js", + "nodeenv": "dotenv -e .env.$APP_ENV -- node server.js", + "prod": "npx next build && dotenv -e .env.production -- node server.js", "build": "next build", "buildWin": "npm run build", "start": "next start", "devNext": "next dev --port 3003 --experimental-https", - "test": "dotenv -e .env.$NODE_ENV -- nodemon --inspect server.js", - "nodeenv": "dotenv -e .env.$NODE_ENV -- node server.js", - "prod": "npx next build && dotenv -e .env.production -- node server.js" + "test": "dotenv -e .env.$NODE_ENV -- nodemon --inspect server.js" }, "author": "Dobromir Popov ", "_moduleAliases": { @@ -97,6 +97,7 @@ "tailwindcss": "^3.4.1", "tw-elements": "^1.1.0", "typescript": "^5", + "uuid": "^9.0.1", "webpack-bundle-analyzer": "^4.10.1", "winston": "^3.11.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz", @@ -109,4 +110,4 @@ "depcheck": "^1.4.7", "prisma": "^5.11.0" } -} +} \ No newline at end of file diff --git a/pages/api/email.ts b/pages/api/email.ts new file mode 100644 index 0000000..f565e5f --- /dev/null +++ b/pages/api/email.ts @@ -0,0 +1,302 @@ +// API endpoint to process email user actions - urls we send in emails to users + +import { getToken } from "next-auth/jwt"; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter, expressWrapper } from "next-connect"; +const common = require('../../src/helpers/common'); +const emailHelper = require('../../src/helpers/email'); +const { v4: uuidv4 } = require('uuid'); +const CON = require("../../src/helpers/const"); + +import fs from 'fs'; +import path from 'path'; +const handlebars = require("handlebars"); + +const router = createRouter(); + + +//action to accept coverme request from email + + +/** + * + * @param req import { NextApiRequest, NextApiResponse } from 'next' + * @param res import { NextApiRequest, NextApiResponse } from 'next' + */ +export default async function handler(req, res) { + const prisma = common.getPrismaClient(); + + const action = req.query.action; + const emailaction = req.query.emailaction; + // Retrieve and validate the JWT token + + //response is a special action that does not require a token + if (action == "email_response") { + switch (emailaction) { + case "coverMeAccept": + //validate shiftId and assignmentId + let shiftId = req.query.shiftId; + let userId = req.query.userId; + let publisher = await prisma.publisher.findUnique({ + where: { + id: userId + } + }); + // Update the user status to accepted + console.log("User: " + publisher.firstName + " " + publisher.lastName + " accepted the CoverMe request"); + + let assignmentPID = req.query.assignmentPID; + if (!shiftId) { + return res.status(400).json({ message: "Shift ID is not provided" }); + } + if (!assignmentPID) { + return res.status(400).json({ message: "Assignment PID is not provided" }); + } + //check if the assignment request is still open + const assignment = await prisma.assignment.findFirst({ + where: { + publicGuid: assignmentPID, + shiftId: parseInt(shiftId), + isConfirmed: false + }, + include: { + shift: { + include: { + cartEvent: { + include: { + location: true + } + }, + assignments: { + include: { + publisher: true + // { + // include: { + // email: true, + // firstName: true, + // lastName: true + // } + // } + } + } + } + }, + publisher: true + } + }); + + if (!assignment) { + const messagePageUrl = `/message?message=${encodeURIComponent('Някой друг вече е отговорил на рази заявка за заместване')}&type=info&caption=${encodeURIComponent('Заявката е вече обработена')}`; + res.redirect(messagePageUrl); + return; + } + + let to = assignment.shift.assignments.map(a => a.publisher.email); + to.push(publisher.email); + + // update the assignment. clear the guid, isConfirmed to true + await prisma.assignment.update({ + where: { + id: assignment.id + }, + data: { + publisherId: userId, + publicGuid: null, // if this exists, we consider the request open + isConfirmed: true + } + }); + const newAssignment = await prisma.assignment.findFirst({ + where: { + shiftId: parseInt(shiftId), + isConfirmed: true + }, + include: { + shift: { + include: { + cartEvent: { + include: { + location: true + } + }, + assignments: { + include: { + publisher: true + } + } + } + } + } + }); + + + const shiftStr = `${CON.weekdaysBG[assignment.shift.startTime.getDay()]} ${CON.GetDateFormat(assignment.shift.startTime)} at ${assignment.shift.cartEvent.location.name} from ${CON.GetTimeFormat(assignment.shift.startTime)} to ${CON.GetTimeFormat(assignment.shift.endTime)}`; + + const newPubs = newAssignment.shift.assignments.map(a => ({ + name: `${a.publisher.firstName} ${a.publisher.lastName}`, + phone: a.publisher.phone + })); + + let model = { + user: publisher, + shiftStr: shiftStr, + shiftId: assignment.shiftId, + prefix: publisher.isMale ? "Брат" : "Сестра", + oldPubName: assignment.publisher.firstName + " " + assignment.publisher.lastName, + firstName: publisher.firstName, + lastName: publisher.lastName, + newPubs: newPubs, + placeName: assignment.shift.cartEvent.location.name, + dateStr: common.getDateFormated(assignment.shift.startTime), + time: common.formatTimeHHmm(assignment.shift.startTime), + sentDate: common.getDateFormated(new Date()) + }; + + emailHelper.SendEmailHandlebars(to, "coverMeAccepted", model); + + const messagePageUrl = `/message?message=${encodeURIComponent('Вашата заявка за замстване е обработена успешно')}&type=info&caption=${encodeURIComponent('Благодарим ви!')}`; + res.redirect(messagePageUrl); + + break; + + //POST + case "send_report": //we can send report form in the emails to the user. process the POSTED data here + // Send report form to the user + //get from POST data: locationId, date, placementCount, videoCount, returnVisitInfoCount, conversationCount + let locationId = req.body.locationId; + let date = req.body.date; + let placementCount = req.body.placementCount; + let videoCount = req.body.videoCount; + let returnVisitInfoCount = req.body.returnVisitInfoCount; + let conversationCount = req.body.conversationCount; + + console.log("User: " + user.email + " sent a report: " + + locationId + " " + date + " " + + placementCount + " " + videoCount + " " + + returnVisitInfoCount + " " + conversationCount); + + //save the report in the database + await prisma.report.create({ + data: { + userId: parseInt(userId), + locationId: parseInt(locationId), + date: date, + placementCount: parseInt(placementCount), + videoCount: parseInt(videoCount), + returnVisitInfoCount: parseInt(returnVisitInfoCount), + conversationCount: parseInt(conversationCount) + } + }); + + break; + } + // //send email response to the user + // const emailResponse = await common.sendEmail(user.email, "Email Action Processed", + // "Your email action was processed successfully"); + } + else { + + const token = await getToken({ req: req }); + if (!token) { + // If no token or invalid token, return unauthorized status + return res.status(401).json({ message: "Unauthorized to call this API endpoint" }); + } + + const user = await prisma.publisher.findUnique({ + where: { + email: token.email + } + }); + + switch (action) { + case "sendCoverMeRequestByEmail": + // Send CoverMe request to the user + //get from POST data: shiftId, assignmentId, date + //let shiftId = req.body.shiftId; + let assignmentId = req.body.assignmentId; + let date = req.body.date; + + console.log("User: " + user.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " " + date); + + let assignment = await prisma.assignment.findUnique({ + where: { + id: parseInt(assignmentId) + }, + include: { + shift: { + include: { + cartEvent: { + include: { + location: true + } + } + } + } + } + }); + + // update the assignment. generate new publicGuid, isConfirmed to false + let newPublicGuid = uuidv4(); + await prisma.assignment.update({ + where: { + id: parseInt(assignmentId) + }, + data: { + publicGuid: newPublicGuid, // if this exists, we consider the request open + isConfirmed: false + } + }); + + //get all subscribed publisers + const subscribedPublishers = await prisma.publisher.findMany({ + where: { + isSubscribedToCoverMe: true + } + }); + //send email to all subscribed publishers + for (let i = 0; i < subscribedPublishers.length; i++) { + if (subscribedPublishers[i].id == user.id) { + continue; + } + + //send email to subscribed publisher + let acceptUrl = process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=coverMeAccept&userId=" + subscribedPublishers[i].id + "&shiftId=" + assignment.shiftId + "&assignmentPID=" + newPublicGuid; + + let model = { + user: user, + shiftId: assignment.shiftId, + acceptUrl: acceptUrl, + prefix: user.isMale ? "Брат" : "Сестра", + firstName: subscribedPublishers[i].firstName, + lastName: subscribedPublishers[i].lastName, + email: subscribedPublishers[i].email, + placeName: assignment.shift.cartEvent.location.name, + dateStr: common.getDateFormated(assignment.shift.startTime), + time: common.formatTimeHHmm(assignment.shift.startTime), + sentDate: common.getDateFormated(new Date()) + }; + let results = emailHelper.SendEmailHandlebars( + { + name: subscribedPublishers[i].firstName + " " + subscribedPublishers[i].lastName, + email: subscribedPublishers[i].email + }, "coverMe", model); + // if (results) { + // console.log("Error sending email: " + error); + // return res.status(500).json({ message: "Error sending email:" + error }); + //} + + if (results) { + console.log("Email sent to: " + subscribedPublishers[i].email); + } + + } + break; + default: + return res.status(400).json({ message: "Invalid action" }); + } + + return res.status(200).json({ message: "User action processed" }); + } + +} +router.use(expressWrapper(handler)); + diff --git a/pages/cart/publishers/myschedule.tsx b/pages/cart/publishers/myschedule.tsx index 273dc56..c7c4ee2 100644 --- a/pages/cart/publishers/myschedule.tsx +++ b/pages/cart/publishers/myschedule.tsx @@ -51,6 +51,23 @@ export default function MySchedulePage({ assignments }) { console.log("error", error); }); }; + + const searchReplacement = (assignmentId) => { + axiosInstance.post('/api/email?action=sendCoverMeRequestByEmail', { + assignmentId: assignmentId, + }).then(response => { + console.log("response", response); + //toast success and confirm the change + toast.success("Заявката за заместник е изпратена!", { + onClose: () => { + window.location.reload(); + } + }); + }).catch(error => { + console.log("error", error); + }); + } + return ( @@ -75,8 +92,9 @@ export default function MySchedulePage({ assignments }) {
{assignment.shift.assignments.map((a, index) => { return ( - - {a.publisher.firstName} {a.publisher.lastName}{a.isWithTransport && } + + {a.publisher.firstName} {a.publisher.lastName} + {a.isWithTransport && } ) } @@ -101,14 +119,14 @@ export default function MySchedulePage({ assignments }) { setIsModalOpen(true) }} > - Заместник + Избери Заместник - {/* */} +
diff --git a/pages/message.tsx b/pages/message.tsx new file mode 100644 index 0000000..a3502a3 --- /dev/null +++ b/pages/message.tsx @@ -0,0 +1,27 @@ +// pages/message.js + +import { useRouter } from 'next/router'; +import Layout from "../components/layout"; + +export default function MessagePage() { + const router = useRouter(); + const messageStyles = { + error: "text-red-500", + warning: "text-yellow-500", + info: "text-blue-500", + }; + const { message, type = messageStyles.info, caption } = router.query; + + return ( + +
+
+

{caption || 'Информация'}

+

+ {message || 'Така ще получавате различни съобщения.'} +

+
+
+
+ ); +} diff --git a/prisma/administrative_scripts/create_user.sql b/prisma/administrative_scripts/create_user.sql new file mode 100644 index 0000000..b521ce0 --- /dev/null +++ b/prisma/administrative_scripts/create_user.sql @@ -0,0 +1,2 @@ +CREATE USER 'cart'@'%' IDENTIFIED BY 'cartpw'; +GRANT ALL PRIVILEGES ON `cart\_dev`.* TO 'cart'@'%' WITH GRANT OPTION; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1faea21..aa7ecdd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,7 +10,6 @@ // //to generate schema // > npx prisma -// GPT // This is a Prisma database schema definition, which describes the structure and relationships between various entities in the database. Here's a brief overview of the different models: // Publisher: Represents a publisher, with attributes such as first name, last name, email, phone, age, and availability. A publisher can have many availabilities and assignments, and can also have multiple user accounts and sessions. @@ -21,9 +20,10 @@ // Location: Represents a location where a cart event can take place. A location can have a name, address, and multiple cart events. // Overall, this schema seems to represent a system for managing publishers and their assignments to cart events, including their availabilities and locations. +//$env:DATABASE="{connection string}"; npx prisma migrate deploy datasource db { provider = "mysql" - url = env("DATABASE_URL") + url = env("DATABASE") } generator client { diff --git a/server.js b/server.js index 8e3982c..a2d5ad9 100644 --- a/server.js +++ b/server.js @@ -20,19 +20,9 @@ process.env.TZ = 'Europe/Sofia'; // Global variable to store the base URL let baseUrlGlobal; -// if (process.env.NODE_ENV === 'test') { -// // Load environment variables from .env.test -// require('dotenv').config({ path: '.env.test' }); -// } else { -// // Load default environment variables -// require('dotenv').config(); -// } - +console.log("initial process.env.APP_ENV = ", process.env.APP_ENV); console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param -require('dotenv').config({ - path: `.env.${process.env.NODE_ENV}` -}); - +require('dotenv').config({ path: `.env.${process.env.APP_ENV}` }); console.log("process.env.NODE_ENV = ", process.env.NODE_ENV); const PROTOCOL = process.env.PROTOCOL; @@ -48,6 +38,8 @@ console.log("process.env.NEXT_PUBLIC_PUBLIC_URL = ", process.env.NEXT_PUBLIC_PUB console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL); console.log("process.env.PORT = ", process.env.PORT); console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT); +console.log("process.env.DATABASE_URL = ", process.env.DATABASE_URL); +console.log("process.env.DATABASE = ", process.env.DATABASE); //require('module-alias/register'); diff --git a/src/helpers/common.js b/src/helpers/common.js index fdac13e..a9a8b24 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -83,14 +83,14 @@ exports.getBaseUrl = function (relative = "", req = null) { let prisma; exports.getPrismaClient = function getPrismaClient() { if (!prisma) { - logger.debug("getPrismaClient: process.env.DATABASE_URL = ", process.env.DATABASE_URL); + logger.debug("getPrismaClient: process.env.DATABASE = ", process.env.DATABASE); prisma = new PrismaClient({ // Optional: Enable logging //log: ['query', 'info', 'warn', 'error'], - datasources: { db: { url: process.env.DATABASE_URL } }, + datasources: { db: { url: process.env.DATABASE } }, }); } - logger.debug("getPrismaClient: process.env.DATABASE_URL = ", process.env.DATABASE_URL); + logger.debug("getPrismaClient: process.env.DATABASE = ", process.env.DATABASE); return prisma; } @@ -525,7 +525,9 @@ exports.getCurrentYearMonth = () => { const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed return `${year}-${month}`; } - +exports.getTimeFormated = function (date) { + return this.formatTimeHHmm(date); +} // format date to 'HH:mm' time string required by the time picker exports.formatTimeHHmm = function (input) { // Check if the input is a string or a Date object @@ -729,3 +731,7 @@ exports.getLocalStorage = function (key, defaultValue) { } return defaultValue; }; + +exports.root = function (req) { + return process.env.NEXT_PUBLIC_PUBLIC_URL; +} diff --git a/src/helpers/email.js b/src/helpers/email.js index f73ef5f..a69db91 100644 --- a/src/helpers/email.js +++ b/src/helpers/email.js @@ -1,10 +1,12 @@ // helper module to send emails with nodemailer const fs = require("fs"); +const path = require('path'); const { MailtrapClient } = require("mailtrap"); const nodemailer = require("nodemailer"); const CON = require("./const"); const CAL = require("./calendar"); +const Handlebars = require('handlebars'); // const { google } = require("googleapis"); // const OAuth2 = google.auth.OAuth2; @@ -12,14 +14,42 @@ const CAL = require("./calendar"); const { Shift, Publisher, PrismaClient } = require("@prisma/client"); const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad"; -const SENDER_EMAIL = "pw@d-popov.com"; -const sender = { name: "JW Cart: Shift Info", email: SENDER_EMAIL }; +const SENDER_EMAIL = "sofia@mwitnessing.com"; +const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL }; const client = new MailtrapClient({ token: TOKEN }); -const mailtrapTestClient = new MailtrapClient({ - username: '8ec69527ff2104',//not working now - password: 'c7bc05f171c96c' -}); +let mailtrapTestClient = null; +// const mailtrapTestClient = new MailtrapClient({ +// username: '8ec69527ff2104',//not working now +// password: 'c7bc05f171c96c' +// }); +//test +var transporter = nodemailer.createTransport({ + host: "sandbox.smtp.mailtrap.io", + port: 2525, + auth: { + user: "8ec69527ff2104", + pass: "c7bc05f171c96c" + } +}); +// production +// var transporter = nodemailer.createTransport({ +// host: "live.smtp.mailtrap.io", +// port: 587, +// auth: { +// user: "api", +// pass: "1cfe82e747b8dc3390ed08bb16e0f48d" +// } +// }); + +var transporterBulk = nodemailer.createTransport({ + host: "bulk.smtp.mailtrap.io", + port: 587, + auth: { + user: "api", + pass: "1cfe82e747b8dc3390ed08bb16e0f48d" + } +}); // ------------------ Email sending ------------------ var lastResult = null; function setResult(result) { @@ -29,44 +59,194 @@ exports.GetLastResult = function () { return lastResult; }; -exports.SendEmail = async function (to, subject, text, html) { - const message = { - from: sender, - to, - subject, - text, - html, - }; -}; +function normalizeEmailAddresses(to) { + let emails = []; -exports.SendEmail_Test = async function (to, subject, text, html) { - const message = { - from: sender, - to, - subject, - text, - html, - }; + if (typeof to === 'string') { + // Handle CSV string by splitting into an array + if (to.includes(',')) emails = to.split(/\s*,\s*/); + else emails = [to]; // Handle single email string + } else if (Array.isArray(to)) { + emails = to.map(item => { + if (typeof item === 'string') return item; + if (item.name && item.email) return `"${item.name}" <${item.email}>`; + return JSON.stringify(item); // Handle unexpected object format + }); + } else if (typeof to === 'object' && to.email) { + // Handle single object + emails = [`"${to.name}" <${to.email}>`]; + } else { + // Fallback for other types + emails = [String(to)]; + } - await mailtrapTestClient - .send(message) - .then(console.log, console.error, setResult); + return emails; // Always returns an array } +exports.SendEmail = async function (to, subject, text, html, attachments = []) { + let sender = '"Специално Свидетелстване София - тест" '; + const emailAddresses = normalizeEmailAddresses(to) + + const message = { + from: sender, + to: emailAddresses, + subject, + text, + html, + attachments + }; + + if (mailtrapTestClient !== null) { + // Assuming mailtrapTestClient is correctly set up to send emails + await mailtrapTestClient + .send(message) + .then(console.log) + .catch(console.error); + + } else { + + let result = await transporter + .sendMail(message) + .then(console.log) + .catch(console.error); + return result; + } + +}; + +exports.SendEmailHandlebars = async function (to, templateName, model, attachments = []) { + try { + // Ensure the sender and mailtrapTestClient are correctly defined or imported + + // Load and compile the main template + const mainTemplateSource = fs.readFileSync(path.join(process.cwd(), 'src', 'templates', 'emails', 'main.hbs'), 'utf8'); + const mainTemplate = Handlebars.compile(mainTemplateSource); + + // Dynamically load and compile the specified template + const templateSource = fs.readFileSync(path.join(process.cwd(), 'src', 'templates', 'emails', `${templateName}.hbs`), 'utf8'); + + // Extract subject and optional text version from the template source + const subjectMatch = templateSource.match(/{{!--\s*Subject:\s*(.*?)\s*--}}/); + const textMatch = templateSource.match(/{{!--\s*Text:\s*([\s\S]*?)\s*--}}/); + + let subject = subjectMatch ? subjectMatch[1].trim() : 'ССС: Известие'; + let textVersion = textMatch ? textMatch[1].trim() : null; + + // Remove the subject and text annotations from the template source + const cleanTemplateSource = templateSource.replace(/{{!-- Subject: .* --}}/, '').replace(/{{!-- Text: [\s\S]*? --}}/, ''); + // const cleanTemplateSource = templateSource + // .replace(/{{!--\s*Subject:.*?--}}\s*/, '') + // .replace(/{{!--\s*Text:.*?--}}\s*/, ''); + // Compile the cleaned template + const template = Handlebars.compile(cleanTemplateSource); + + // Render the specified template with the provided model + const templateHtml = template(model); + + // Render the main template, inserting the specific template HTML + const html = mainTemplate({ body: templateHtml }); + + // Generate a plain text version if not explicitly provided + let text = textVersion || html.replace(/<[^>]*>?/gm, ''); // Simple regex to strip HTML tags. Might need refinement. + subject = Handlebars.compile(subject)(model); + text = Handlebars.compile(text)(model); + + let results = this.SendEmail(to, subject, text, html, attachments); + return results; + + } catch (error) { + console.error(error); + return new Error('Error sending email'); + } +}; + + +exports.SendEmail_NewShifts = async function (publisher, shifts) { + if (shifts.length === 0) return; + + var date = new Date(shifts[0].startTime); + + // Generate ICS calendar links for all shifts + const icsLink = CAL.GenerateICS(shifts); + + // Prepare the shifts string + const shiftStr = shifts.map((s) => { + return `${CON.weekdaysBG[s.startTime.getDay()]} ${CON.GetDateFormat(s.startTime)} at ${s.cartEvent.location.name} from ${CON.GetTimeFormat(s.startTime)} to ${CON.GetTimeFormat(s.endTime)}`; + }).join("
"); + + // Define the model for the Handlebars template + const model = { + publisherFirstName: publisher.firstName, + publisherLastName: publisher.lastName, + month: CON.monthNamesBG[date.getMonth()], + shifts: shiftStr, + sentDate: new Date().toLocaleDateString() // Assuming you want to include the sent date in the email + }; + + await exports.SendEmailHandlebars(publisher.email, "newShifts", model, + [{ + filename: "calendar.ics", + content: icsLink, + contentType: 'text/calendar' // Ensure this is correctly set for the ICS file + }] + ).then(console.log).catch(console.error); +}; + + + + +//----------------------- OLD ----------------------------- + +// exports.SendEmail_NewShifts = async function (publisher, shifts) { +// if (shifts.length == 0) return; + +// var date = new Date(shifts[0].startTime); + +// //generate ICS calendar links for all shifts +// const icsLink = CAL.GenerateICS(shifts); + +// const shftStr = shifts +// .map((s) => { +// return ` ${CON.weekdaysBG[s.startTime.getDay()] +// } ${CON.GetDateFormat(s.startTime)} ${s.cartEvent.location.name +// } ${CON.GetTimeFormat(s.startTime)} - ${CON.GetTimeFormat( +// s.endTime +// )}`; +// }) +// .join("\n"); + +// await client.send({ +// from: sender, +// to: [ +// { +// email: "dobromir.popov@gmail.com",//publisher.email, +// name: publisher.firstName + " " + publisher.lastName, +// }, +// ], +// subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()], +// text: +// "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" + +// "Ти регистриран да получавате известия за нови смени на количка.\n" + +// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` + +// ` ${shftStr} \n\n\n` + +// "Поздрави,\n" + +// "Специално Свидетелстване София", +// attachments: [ +// { +// filename: "calendar.ics", +// content_id: "calendar.ics", +// disposition: "inline", +// content: icsLink, +// }, +// ], +// }) +// .then(console.log, console.error, setResult); +// }; + + // https://mailtrap.io/blog/sending-emails-with-nodemailer/ -exports.SendTestEmail = async function (to) { - // await client - // .send({ - // from: sender, - // to: [{ email: RECIPIENT_EMAIL }], - // subject: "Hello from Mailtrap!", - // text: "Welcome to Mailtrap Sending!",Shift Info" - // }) - // .then(console.log, console.error, setResult); - - // return lastResult; - +exports.SendEmail_Example = async function (to) { const welcomeImage = fs.readFileSync( path.join(CON.contentPath, "welcome.png") ); @@ -113,50 +293,3 @@ exports.SendTestEmail = async function (to) { }) .then(console.log, console.error, setResult); }; - - -exports.SendEmail_NewShifts = async function (publisher, shifts) { - if (shifts.length == 0) return; - - var date = new Date(shifts[0].startTime); - - //generate ICS calendar links for all shifts - const icsLink = CAL.GenerateICS(shifts); - - const shftStr = shifts - .map((s) => { - return ` ${CON.weekdaysBG[s.startTime.getDay()] - } ${CON.GetDateFormat(s.startTime)} ${s.cartEvent.location.name - } ${CON.GetTimeFormat(s.startTime)} - ${CON.GetTimeFormat( - s.endTime - )}`; - }) - .join("\n"); - - await client.send({ - from: sender, - to: [ - { - email: "dobromir.popov@gmail.com",//publisher.email, - name: publisher.firstName + " " + publisher.lastName, - }, - ], - subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()], - text: - "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" + - "Ти регистриран да получавате известия за нови смени на количка.\n" + - `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` + - ` ${shftStr} \n\n\n` + - "Поздрави,\n" + - "Специално Свидетелстване София", - attachments: [ - { - filename: "calendar.ics", - content_id: "calendar.ics", - disposition: "inline", - content: icsLink, - }, - ], - }) - .then(console.log, console.error, setResult); -}; diff --git a/src/templates/emails/coverMe.hbs b/src/templates/emails/coverMe.hbs new file mode 100644 index 0000000..2855a11 --- /dev/null +++ b/src/templates/emails/coverMe.hbs @@ -0,0 +1,26 @@ +{{!--Subject: ССС: Нужен е заместник --}} + +
+

Търси се зместник: + {{!-- за смяна на {{placeName}} за {{dateStr}}! --}} +

+

Здравей {{firstName}},

+

{{prefix}} {{user.firstName}} {{user.lastName}} търси заместник.

+ {{!--

Shift Details:

--}} +

Дата: {{dateStr}}
Час: {{time}}
Място: {{placeName}}

+

С натискането на бутона по-долу можеш да премеш да го заместваш. + {{!-- Ти, той/тя и останалите участници в смяната ще + получат имейл за промяната. Твоята помощ е много ценна. --}} +

+

+ Ще + поема смяната +

+ {{!--

Thank you very much for considering my request.

+

Best regards,
{{name}}

--}} +
+
+

Изпратено до {{firstName}} {{lastName}} {{email}} {{sentDate}}

+
\ No newline at end of file diff --git a/src/templates/emails/coverMeAccepted.hbs b/src/templates/emails/coverMeAccepted.hbs new file mode 100644 index 0000000..16bb198 --- /dev/null +++ b/src/templates/emails/coverMeAccepted.hbs @@ -0,0 +1,19 @@ +{{!-- Subject: ССС: Промени в твоята смяна --}} + +
+

Промяна твоята смяна на {{placeName}} {{dateStr}}

+

Здравейте {{firstName}},

+

{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}

+

Новаия списък с участници за тази смяна е:

+
    + {{#each newPubs}} +
  • {{this.name}} - {{this.phone}}
  • + {{/each}} +
+
+
+
+ +{{!--
+ Изпратено на: {{sentDate}} +
--}} \ No newline at end of file diff --git a/src/templates/emails/example.hbs b/src/templates/emails/example.hbs new file mode 100644 index 0000000..e300167 --- /dev/null +++ b/src/templates/emails/example.hbs @@ -0,0 +1,24 @@ +{{!-- Subject: ССС: Нужен е заместник--}} +{{!-- Text: Plain text version of your email. If not provided, HTML tags will be stripped from the HTML version for the +text version. --}} + +
+

Търси се зместник за смяна на {{placeName}} за {{dateStr}}!

+

Здравейте,

+

{{prefix}} {{firstName}} {{lastName}} търси заместник.

+ {{!--

Shift Details:

--}} +

Дата: {{dateStr}}
Час: {{time}}
Място: {{placeName}}

+

С натискането на бутона по-долу можете да премете да го замествате. Вие, той/тя и останалите участници в смяната + ще бъдат уведумени чрез имейл за промяната. Вашата помощ е много ценна.

+

+ Ще + поема смяната +

+ {{!--

Thank you very much for considering my request.

+

Best regards,
{{name}}

--}} +
+
+

Изпратено на: {{sentDate}}

+
\ No newline at end of file diff --git a/src/templates/emails/main.hbs b/src/templates/emails/main.hbs new file mode 100644 index 0000000..1669557 --- /dev/null +++ b/src/templates/emails/main.hbs @@ -0,0 +1,25 @@ + + + + + + + ССС известия + + + +
+

Cпециално Свидетелстване София

+
+ +
+ {{{body}}} +
+ +
+ © 2024 ССС. All rights reserved. +
+ + + \ No newline at end of file diff --git a/src/templates/emails/newShifts.hbs b/src/templates/emails/newShifts.hbs new file mode 100644 index 0000000..5f52bc8 --- /dev/null +++ b/src/templates/emails/newShifts.hbs @@ -0,0 +1,14 @@ +{{!-- Subject: ССС: Нови назначени смени--}} + +
+

Здравейте, {{publisherFirstName}} {{publisherLastName}}!

+

Ти регистриран да получавате известия за нови смени на количка.

+

За месец {{month}} имате следните смени:

+
+ {{{shifts}}} +
+
+ + \ No newline at end of file