Merge commit '0717433acbed4c1b3051f7ceec6c8d2913cc9441' into production
This commit is contained in:
7
.env
7
.env
@ -6,13 +6,8 @@
|
|||||||
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
||||||
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
||||||
|
|
||||||
|
NODE_ENV=development
|
||||||
# mysql
|
# 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 | Специално Свидетелстване София
|
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
||||||
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||||
|
@ -4,9 +4,8 @@ PROTOCOL=https
|
|||||||
PORT=3003
|
PORT=3003
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
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_KEY=./certificates/localhost-key.pem
|
||||||
SSL_CERT=./certificates/localhost.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
|
|
10
.env.development.raph
Normal file
10
.env.development.raph
Normal file
@ -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
|
@ -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
|
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
||||||
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||||
DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
@ -1,3 +1,5 @@
|
|||||||
|
NODE_ENV=test
|
||||||
|
|
||||||
PROTOCOL=http
|
PROTOCOL=http
|
||||||
HOST=staging.mwitnessing.com
|
HOST=staging.mwitnessing.com
|
||||||
PORT=
|
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
|
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
||||||
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
# ? 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_ID=
|
||||||
APPLE_TEAM_ID=
|
APPLE_TEAM_ID=
|
||||||
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@ -5,13 +5,23 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Run npm nodemon (DEV)",
|
"name": "Run npm nodemon (DB)",
|
||||||
"command": "npm run debug",
|
"command": "npm run debug",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"preLaunchTask": "killInspector",
|
"preLaunchTask": "killInspector",
|
||||||
"env": {
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ services:
|
|||||||
- /mnt/apps/docker_volumes/cart/app/next-cart-app:/app
|
- /mnt/apps/docker_volumes/cart/app/next-cart-app:/app
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=demo
|
- 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 "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 "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"
|
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"
|
||||||
|
@ -8,7 +8,7 @@ services:
|
|||||||
- /mnt/apps/DEV/cart-demo:/app
|
- /mnt/apps/DEV/cart-demo:/app
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=demo
|
- 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"
|
command: sh -c " cd /app && npm run test; tail -f /dev/null"
|
||||||
tty: true
|
tty: true
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
nextjs-app: # https://sofia.mwitnessing.com/
|
nextjs-app: # https://sofia.mwhitnessing.com/
|
||||||
hostname: jwpw-app-staging # jwpw-nextjs-app-1
|
hostname: jwpw-app-staging # jwpw-nextjs-app-1
|
||||||
image: docker.d-popov.com/jwpw:latest
|
image: docker.d-popov.com/jwpw:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/docker_volumes/pw-demo/app/public/content/uploads/:/app/public/content/uploads
|
- /mnt/docker_volumes/pw-demo/app/public/content/uploads/:/app/public/content/uploads
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=demo
|
- APP_ENV=test
|
||||||
|
- NODE_ENV=test
|
||||||
- TZ=Europe/Sofia
|
- 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
|
- UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git
|
||||||
- GIT_BRANCH=main
|
- GIT_BRANCH=main
|
||||||
- GIT_USERNAME=deploy
|
- GIT_USERNAME=deploy
|
||||||
@ -21,19 +22,17 @@ services:
|
|||||||
- infrastructure_default
|
- infrastructure_default
|
||||||
mariadb:
|
mariadb:
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 0
|
replicas: 1
|
||||||
hostname: mariadb-demo
|
hostname: mariadb-demo
|
||||||
image: mariadb:latest #mariadb:10.4
|
image: mysql:latest #mariadb:10.4
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/docker_volumes/pw-demo/data/mysql:/var/lib/mysql
|
- /mnt/docker_volumes/pw-demo2/data/mysql:/var/lib/mysql
|
||||||
environment:
|
environment:
|
||||||
MARIADB_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93
|
MARIADB_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93
|
||||||
MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93
|
MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93
|
||||||
MYSQL_DATABASE: jwpwsofia_demo
|
MYSQL_DATABASE: jwpwsofia_demo
|
||||||
MYSQL_USER: jwpwsofia_demo
|
MYSQL_USER: jwpwsofia_demo
|
||||||
MYSQL_PASSWORD: dwxhns9p9vp248
|
MYSQL_PASSWORD: dwxhns9p9vp248
|
||||||
networks:
|
|
||||||
- infrastructure_default
|
|
||||||
networks:
|
networks:
|
||||||
infrastructure_default:
|
infrastructure_default:
|
||||||
external: true
|
external: true
|
||||||
|
@ -12,8 +12,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- TZ=Europe/Sofia
|
- TZ=Europe/Sofia
|
||||||
- DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
- DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
||||||
#- DATABASE_URL=postgres://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
|
- UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git
|
||||||
- GIT_BRANCH=production
|
- GIT_BRANCH=production
|
||||||
- GIT_USERNAME=deploy
|
- GIT_USERNAME=deploy
|
||||||
|
@ -6,7 +6,7 @@ services:
|
|||||||
- "5001:3000"
|
- "5001:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=prod
|
- 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
|
- UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git
|
||||||
- GIT_USERNAME=deploy
|
- GIT_USERNAME=deploy
|
||||||
- GIT_PASSWORD=%L3Kr2R438u4F7^%40
|
- GIT_PASSWORD=%L3Kr2R438u4F7^%40
|
||||||
|
@ -4,6 +4,8 @@ FROM node:current-alpine
|
|||||||
|
|
||||||
# Set environment variables for Node.js
|
# Set environment variables for Node.js
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
# ENV MYSQL_ROOT_PASSWORD=pass
|
||||||
|
ENV MYSQL_DATABASE=cart
|
||||||
|
|
||||||
# Create and set the working directory
|
# Create and set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -40,7 +40,7 @@ services:
|
|||||||
- /mnt/data/apps/docker_volumes/cart/app:/app
|
- /mnt/data/apps/docker_volumes/cart/app:/app
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=demo
|
- NODE_ENV=demo
|
||||||
- DATABASE_URL=mysql://cart:cartpw2024@mariadb:3306/cart
|
- DATABASE=mysql://cart:cartpw2024@mariadb:3306/cart
|
||||||
#! entrypoint: ["/bin/sh", "/entrypoint.sh"]
|
#! entrypoint: ["/bin/sh", "/entrypoint.sh"]
|
||||||
#run: npm install && npx prisma generate && npm run test;
|
#run: npm install && npx prisma generate && npm run test;
|
||||||
# command: "npx prisma migrate deploy && npx prisma migrate deploy && npm run build && npm run start"
|
# command: "npx prisma migrate deploy && npx prisma migrate deploy && npm run build && npm run start"
|
||||||
|
@ -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
|
fix repeating availabilities - Tanq kolcjanova only blue first thursday
|
||||||
add assignment in calendar planner
|
add assignment in calendar planner
|
||||||
fix database
|
fix database
|
||||||
|
|
||||||
|
--
|
||||||
|
emails
|
||||||
|
mobile apps
|
||||||
|
apple login
|
||||||
|
разрешителни - upload
|
||||||
|
@ -111,6 +111,11 @@ export OPENAI_API_KEY=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 # dev-
|
|||||||
|
|
||||||
# ----------------------------------------------update PRISMA schema/sync database ----------------------------------------------- #
|
# ----------------------------------------------update PRISMA schema/sync database ----------------------------------------------- #
|
||||||
# prisma migrate dev --create-only
|
# 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 generate
|
||||||
npx prisma migrate dev --name fix_nextauth_schema --create-only
|
npx prisma migrate dev --name fix_nextauth_schema --create-only
|
||||||
>Prisma Migrate created the following migration without applying it 20231214163235_fix_nextauth_schema
|
>Prisma Migrate created the following migration without applying it 20231214163235_fix_nextauth_schema
|
||||||
@ -196,3 +201,8 @@ ncu -u
|
|||||||
|
|
||||||
enable apple ID:
|
enable apple ID:
|
||||||
curl https://gist.githubusercontent.com/balazsorban44/09613175e7b37ec03f676dcefb7be5eb/raw/b0d31aa0c7f58e0088fdf59ec30cad1415a3475b/apple-gen-secret.mjs -o apple-gen-secret.mjs
|
curl https://gist.githubusercontent.com/balazsorban44/09613175e7b37ec03f676dcefb7be5eb/raw/b0d31aa0c7f58e0088fdf59ec30cad1415a3475b/apple-gen-secret.mjs -o apple-gen-secret.mjs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Project setup:
|
||||||
|
@ -25,6 +25,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
indexUrl: "/cart/availabilities"
|
indexUrl: "/cart/availabilities"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//coalsce existingItems to empty array
|
||||||
|
existingItems = existingItems || [];
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState(existingItems.length > 0);
|
const [editMode, setEditMode] = useState(existingItems.length > 0);
|
||||||
const [publisher, setPublisher] = useState({ id: publisherId });
|
const [publisher, setPublisher] = useState({ id: publisherId });
|
||||||
const [day, setDay] = useState(new Date(date));
|
const [day, setDay] = useState(new Date(date));
|
||||||
|
@ -57,13 +57,13 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
// Update internal state when `events` prop changes
|
// Update internal state when `events` prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//if we have isBySystem - set type to assignment
|
//if we have isBySystem - set type to assignment
|
||||||
let updatedEvents = events.map(event => {
|
let updatedEvents = events?.map(event => {
|
||||||
if (event.isBySystem) {
|
if (event.isBySystem) {
|
||||||
event.type = "assignment";
|
event.type = "assignment";
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
});
|
});
|
||||||
updatedEvents = events.map(event => ({
|
updatedEvents = events?.map(event => ({
|
||||||
...event,
|
...event,
|
||||||
date: new Date(event.startTime).setHours(0, 0, 0, 0),
|
date: new Date(event.startTime).setHours(0, 0, 0, 0),
|
||||||
startTime: new Date(event.startTime),
|
startTime: new Date(event.startTime),
|
||||||
|
@ -185,10 +185,13 @@ export default function PublisherForm({ item, me }) {
|
|||||||
<label className="label" htmlFor="lastName">Фамилия</label>
|
<label className="label" htmlFor="lastName">Фамилия</label>
|
||||||
<input type="text" name="lastName" value={publisher.lastName} onChange={handleChange} className="textbox" placeholder="Last Name" autoFocus />
|
<input type="text" name="lastName" value={publisher.lastName} onChange={handleChange} className="textbox" placeholder="Last Name" autoFocus />
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
||||||
<div className="form-check">
|
|
||||||
<input className="checkbox" type="checkbox" value={publisher.isNameForeign} id="isNameForeign" name="isNameForeign" onChange={handleChange} checked={publisher.isNameForeign} autoComplete="off" />
|
<div className="border border-blue-500 border-solid p-2">
|
||||||
<label className="label" htmlFor="isNameForeign">
|
<div className="form-check">
|
||||||
Чуждестранна фамилия</label>
|
<input className="checkbox" type="checkbox" value={publisher.isNameForeign} id="isNameForeign" name="isNameForeign" onChange={handleChange} checked={publisher.isNameForeign} autoComplete="off" />
|
||||||
|
<label className="label" htmlFor="isNameForeign">
|
||||||
|
Чуждестранна фамилия</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
</div>
|
</div>
|
||||||
@ -232,60 +235,68 @@ export default function PublisherForm({ item, me }) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
</div>
|
||||||
<label className="label" htmlFor="type">Тип</label>
|
<div className="mb-4">
|
||||||
<select name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
<label className="label" htmlFor="town">Град</label>
|
||||||
<option value="Publisher">Вестител</option>
|
<input type="text" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
||||||
<option value="Bethelite">Бетелит</option>
|
</div>
|
||||||
<option value="RegularPioneer">Редовен Пионер</option>
|
<div className="mb-4">
|
||||||
<option value="SpecialPioneer_Missionary">Специален Пионер/Мисионер</option>
|
<div className="form-check">
|
||||||
{/* <option value="Missionary">Мисионер</option>
|
<input className="checkbox" type="checkbox" id="isSubscribedToCoverMe" name="isSubscribedToCoverMe" onChange={handleChange} checked={publisher.isSubscribedToCoverMe} autoComplete="off" />
|
||||||
<option value="CircuitOverseer">Пътуваща служба</option> */}
|
<label className="label" htmlFor="isSubscribedToCoverMe">Абониран за имейли за заместване</label>
|
||||||
</select>
|
<input className="checkbox" type="checkbox" id="isSubscribedToReminders" name="isSubscribedToReminders" onChange={handleChange} checked={publisher.isSubscribedToReminders} autoComplete="off" />
|
||||||
</div>
|
<label className="label" htmlFor="isSubscribedToReminders">Абониран за напомняния (имейл)</label>
|
||||||
<div className="mb-4">
|
|
||||||
<label className="label" htmlFor="town">Град</label>
|
|
||||||
<input type="text" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="label" htmlFor="comments">Коментари</label>
|
|
||||||
<input type="text" name="comments" value={publisher.comments} onChange={handleChange} className="textbox" placeholder="Коментари" autoFocus />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
|
||||||
<div className="mb-4">
|
{/* ADMINISTRATORS ONLY */}
|
||||||
<label className="label" htmlFor="age">Възраст</label>
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
||||||
<input type="number" name="age" value={publisher.age} onChange={handleChange} className="textbox" placeholder="Age" autoFocus />
|
<div className="border border-blue-500 border-solid p-2">
|
||||||
</div>
|
<div className="mb-4">
|
||||||
<div className="mb-4">
|
<label className="label" htmlFor="type">Тип</label>
|
||||||
<div className="form-check">
|
<select name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
||||||
<input className="checkbox" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={publisher.isActive} autoComplete="off" />
|
<option value="Publisher">Вестител</option>
|
||||||
<label className="label" htmlFor="isActive">Активен</label>
|
<option value="Bethelite">Бетелит</option>
|
||||||
<input className="checkbox" type="checkbox" id="isTrained" name="isTrained" onChange={handleChange} checked={publisher.isTrained} autoComplete="off" />
|
<option value="RegularPioneer">Редовен Пионер</option>
|
||||||
<label className="label" htmlFor="isTrained">Получил обучение</label>
|
<option value="SpecialPioneer_Missionary">Специален Пионер/Мисионер</option>
|
||||||
<input className="checkbox disabled" type="checkbox" id="isImported" name="isImported" onChange={handleChange} checked={publisher.isImported} autoComplete="off" />
|
{/* <option value="Missionary">Мисионер</option>
|
||||||
<label className="label " htmlFor="isImported">Импортиран от график</label>
|
<option value="CircuitOverseer">Пътуваща служба</option> */}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="label" htmlFor="comments">Коментари</label>
|
||||||
|
<input type="text" name="comments" value={publisher.comments} onChange={handleChange} className="textbox" placeholder="Коментари" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="label" htmlFor="age">Възраст</label>
|
||||||
|
<input type="number" name="age" value={publisher.age} onChange={handleChange} className="textbox" placeholder="Age" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="form-check">
|
||||||
|
<input className="checkbox" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={publisher.isActive} autoComplete="off" />
|
||||||
|
<label className="label" htmlFor="isActive">Активен</label>
|
||||||
|
<input className="checkbox" type="checkbox" id="isTrained" name="isTrained" onChange={handleChange} checked={publisher.isTrained} autoComplete="off" />
|
||||||
|
<label className="label" htmlFor="isTrained">Получил обучение</label>
|
||||||
|
<input className="checkbox disabled" type="checkbox" id="isImported" name="isImported" onChange={handleChange} checked={publisher.isImported} autoComplete="off" />
|
||||||
|
<label className="label " htmlFor="isImported">Импортиран от график</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="label" htmlFor="role">Роля Потребител</label>
|
||||||
|
<select name="role" id="role" className="select" value={publisher.role} onChange={handleChange} >
|
||||||
|
{/* <option value='${UserRole.USER}'>Потребител</option> */}
|
||||||
|
<option value={`${UserRole.USER}`}>Потребител</option>
|
||||||
|
<option value={`${UserRole.EXTERNAL}`}>Външен</option>
|
||||||
|
<option value={`${UserRole.POWERUSER}`}>Организатор</option>
|
||||||
|
<option value={`${UserRole.ADMIN}`}>Администратор</option>
|
||||||
|
{/* Add other roles as needed */}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<a href="https://t.me/mwHitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
||||||
|
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
||||||
|
<span className="align-middle">Телеграм</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
|
||||||
<label className="label" htmlFor="role">Роля Потребител</label>
|
|
||||||
<select name="role" id="role" className="select" value={publisher.role} onChange={handleChange} >
|
|
||||||
{/* <option value='${UserRole.USER}'>Потребител</option> */}
|
|
||||||
<option value={`${UserRole.USER}`}>Потребител</option>
|
|
||||||
<option value={`${UserRole.EXTERNAL}`}>Външен</option>
|
|
||||||
<option value={`${UserRole.POWERUSER}`}>Организатор</option>
|
|
||||||
<option value={`${UserRole.ADMIN}`}>Администратор</option>
|
|
||||||
{/* Add other roles as needed */}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<a href="https://t.me/mwHitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
|
||||||
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
|
||||||
<span className="align-middle">Телеграм</span>
|
|
||||||
</a>
|
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
{/* ---------------------------- Actions --------------------------------- */}
|
{/* ---------------------------- Actions --------------------------------- */}
|
||||||
<div className="panel-actions">
|
<div className="panel-actions">
|
||||||
@ -312,7 +323,7 @@ export default function PublisherForm({ item, me }) {
|
|||||||
<AvailabilityList publisher={publisher} />
|
<AvailabilityList publisher={publisher} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div >
|
||||||
</div >
|
</div >
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^1.4.0",
|
"@auth/prisma-adapter": "^1.4.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
@ -80,6 +80,7 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tw-elements": "^1.1.0",
|
"tw-elements": "^1.1.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.1",
|
"webpack-bundle-analyzer": "^4.10.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz",
|
||||||
|
13
package.json
13
package.json
@ -9,15 +9,15 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://git.d-popov.com/popov/next-cart-app",
|
"homepage": "https://git.d-popov.com/popov/next-cart-app",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"debug": "nodemon --inspect server.js",
|
"debug": "node server.js",
|
||||||
"debug-env-dev": "dotenv -e .env.development -- nodemon --inspect 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",
|
"build": "next build",
|
||||||
"buildWin": "npm run build",
|
"buildWin": "npm run build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"devNext": "next dev --port 3003 --experimental-https",
|
"devNext": "next dev --port 3003 --experimental-https",
|
||||||
"test": "dotenv -e .env.$NODE_ENV -- nodemon --inspect server.js",
|
"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"
|
|
||||||
},
|
},
|
||||||
"author": "Dobromir Popov <me@d-popov.com>",
|
"author": "Dobromir Popov <me@d-popov.com>",
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
@ -97,6 +97,7 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tw-elements": "^1.1.0",
|
"tw-elements": "^1.1.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.1",
|
"webpack-bundle-analyzer": "^4.10.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz",
|
||||||
@ -109,4 +110,4 @@
|
|||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"prisma": "^5.11.0"
|
"prisma": "^5.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
302
pages/api/email.ts
Normal file
302
pages/api/email.ts
Normal file
@ -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<NextApiRequest, NextApiResponse>();
|
||||||
|
|
||||||
|
|
||||||
|
//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));
|
||||||
|
|
@ -51,6 +51,23 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
console.log("error", error);
|
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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
||||||
@ -75,8 +92,9 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
<dd className="mt-1 text text-gray-900 sm:mt-0 sm:col-span-2">
|
<dd className="mt-1 text text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
{assignment.shift.assignments.map((a, index) => {
|
{assignment.shift.assignments.map((a, index) => {
|
||||||
return (
|
return (
|
||||||
<span key={index} className="inline-flex items-center mr-1 px-1 py-0.5 rounded-full text-sm font-medium ">
|
<span key={index} className="inline-flex items-center mr-1 px-2 py-0.5 border border-gray-300 rounded-full text-sm font-medium bg-gray-100">
|
||||||
{a.publisher.firstName} {a.publisher.lastName}{a.isWithTransport && <LocalShippingIcon style={{ paddingLeft: '4px' }} />}
|
{a.publisher.firstName} {a.publisher.lastName}
|
||||||
|
{a.isWithTransport && <LocalShippingIcon style={{ marginLeft: '4px' }} />}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -101,14 +119,14 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
setIsModalOpen(true)
|
setIsModalOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Заместник
|
Избери Заместник
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
<button
|
||||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
className="inline-flex items-center px-3 mx-2 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
onClick={() => searchReplacement(assignment.id)}
|
onClick={() => searchReplacement(assignment.id)}
|
||||||
>
|
>
|
||||||
Търси заместник
|
Търси заместник
|
||||||
</button> */}
|
</button>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
27
pages/message.tsx
Normal file
27
pages/message.tsx
Normal file
@ -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 (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className={`text-2xl font-bold mb-4 ${messageStyles[type]}`}>{caption || 'Информация'}</h1>
|
||||||
|
<p className="mb-6">
|
||||||
|
{message || 'Така ще получавате различни съобщения.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
2
prisma/administrative_scripts/create_user.sql
Normal file
2
prisma/administrative_scripts/create_user.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
CREATE USER 'cart'@'%' IDENTIFIED BY 'cartpw';
|
||||||
|
GRANT ALL PRIVILEGES ON `cart\_dev`.* TO 'cart'@'%' WITH GRANT OPTION;
|
@ -10,7 +10,6 @@
|
|||||||
// //to generate schema
|
// //to generate schema
|
||||||
// > npx prisma
|
// > 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:
|
// 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.
|
// 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.
|
// 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.
|
// 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 {
|
datasource db {
|
||||||
provider = "mysql"
|
provider = "mysql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE")
|
||||||
}
|
}
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
|
16
server.js
16
server.js
@ -20,19 +20,9 @@ process.env.TZ = 'Europe/Sofia';
|
|||||||
// Global variable to store the base URL
|
// Global variable to store the base URL
|
||||||
let baseUrlGlobal;
|
let baseUrlGlobal;
|
||||||
|
|
||||||
// if (process.env.NODE_ENV === 'test') {
|
console.log("initial process.env.APP_ENV = ", process.env.APP_ENV);
|
||||||
// // 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.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param
|
console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param
|
||||||
require('dotenv').config({
|
require('dotenv').config({ path: `.env.${process.env.APP_ENV}` });
|
||||||
path: `.env.${process.env.NODE_ENV}`
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("process.env.NODE_ENV = ", process.env.NODE_ENV);
|
console.log("process.env.NODE_ENV = ", process.env.NODE_ENV);
|
||||||
|
|
||||||
const PROTOCOL = process.env.PROTOCOL;
|
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.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
|
||||||
console.log("process.env.PORT = ", process.env.PORT);
|
console.log("process.env.PORT = ", process.env.PORT);
|
||||||
console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT);
|
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');
|
//require('module-alias/register');
|
||||||
|
|
||||||
|
@ -83,14 +83,14 @@ exports.getBaseUrl = function (relative = "", req = null) {
|
|||||||
let prisma;
|
let prisma;
|
||||||
exports.getPrismaClient = function getPrismaClient() {
|
exports.getPrismaClient = function getPrismaClient() {
|
||||||
if (!prisma) {
|
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({
|
prisma = new PrismaClient({
|
||||||
// Optional: Enable logging
|
// Optional: Enable logging
|
||||||
//log: ['query', 'info', 'warn', 'error'],
|
//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;
|
return prisma;
|
||||||
}
|
}
|
||||||
@ -525,7 +525,9 @@ exports.getCurrentYearMonth = () => {
|
|||||||
const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||||
return `${year}-${month}`;
|
return `${year}-${month}`;
|
||||||
}
|
}
|
||||||
|
exports.getTimeFormated = function (date) {
|
||||||
|
return this.formatTimeHHmm(date);
|
||||||
|
}
|
||||||
// format date to 'HH:mm' time string required by the time picker
|
// format date to 'HH:mm' time string required by the time picker
|
||||||
exports.formatTimeHHmm = function (input) {
|
exports.formatTimeHHmm = function (input) {
|
||||||
// Check if the input is a string or a Date object
|
// Check if the input is a string or a Date object
|
||||||
@ -729,3 +731,7 @@ exports.getLocalStorage = function (key, defaultValue) {
|
|||||||
}
|
}
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.root = function (req) {
|
||||||
|
return process.env.NEXT_PUBLIC_PUBLIC_URL;
|
||||||
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
// helper module to send emails with nodemailer
|
// helper module to send emails with nodemailer
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require('path');
|
||||||
const { MailtrapClient } = require("mailtrap");
|
const { MailtrapClient } = require("mailtrap");
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const CON = require("./const");
|
const CON = require("./const");
|
||||||
const CAL = require("./calendar");
|
const CAL = require("./calendar");
|
||||||
|
const Handlebars = require('handlebars');
|
||||||
|
|
||||||
// const { google } = require("googleapis");
|
// const { google } = require("googleapis");
|
||||||
// const OAuth2 = google.auth.OAuth2;
|
// const OAuth2 = google.auth.OAuth2;
|
||||||
@ -12,14 +14,42 @@ const CAL = require("./calendar");
|
|||||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||||
|
|
||||||
const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
||||||
const SENDER_EMAIL = "pw@d-popov.com";
|
const SENDER_EMAIL = "sofia@mwitnessing.com";
|
||||||
const sender = { name: "JW Cart: Shift Info", email: SENDER_EMAIL };
|
const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL };
|
||||||
const client = new MailtrapClient({ token: TOKEN });
|
const client = new MailtrapClient({ token: TOKEN });
|
||||||
const mailtrapTestClient = new MailtrapClient({
|
let mailtrapTestClient = null;
|
||||||
username: '8ec69527ff2104',//not working now
|
// const mailtrapTestClient = new MailtrapClient({
|
||||||
password: 'c7bc05f171c96c'
|
// 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 ------------------
|
// ------------------ Email sending ------------------
|
||||||
var lastResult = null;
|
var lastResult = null;
|
||||||
function setResult(result) {
|
function setResult(result) {
|
||||||
@ -29,44 +59,194 @@ exports.GetLastResult = function () {
|
|||||||
return lastResult;
|
return lastResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.SendEmail = async function (to, subject, text, html) {
|
function normalizeEmailAddresses(to) {
|
||||||
const message = {
|
let emails = [];
|
||||||
from: sender,
|
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.SendEmail_Test = async function (to, subject, text, html) {
|
if (typeof to === 'string') {
|
||||||
const message = {
|
// Handle CSV string by splitting into an array
|
||||||
from: sender,
|
if (to.includes(',')) emails = to.split(/\s*,\s*/);
|
||||||
to,
|
else emails = [to]; // Handle single email string
|
||||||
subject,
|
} else if (Array.isArray(to)) {
|
||||||
text,
|
emails = to.map(item => {
|
||||||
html,
|
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
|
return emails; // Always returns an array
|
||||||
.send(message)
|
|
||||||
.then(console.log, console.error, setResult);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
||||||
|
let sender = '"Специално Свидетелстване София - тест" <demo@mwitnessing.com>';
|
||||||
|
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("<br>");
|
||||||
|
|
||||||
|
// 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/
|
// https://mailtrap.io/blog/sending-emails-with-nodemailer/
|
||||||
exports.SendTestEmail = async function (to) {
|
exports.SendEmail_Example = 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;
|
|
||||||
|
|
||||||
const welcomeImage = fs.readFileSync(
|
const welcomeImage = fs.readFileSync(
|
||||||
path.join(CON.contentPath, "welcome.png")
|
path.join(CON.contentPath, "welcome.png")
|
||||||
);
|
);
|
||||||
@ -113,50 +293,3 @@ exports.SendTestEmail = async function (to) {
|
|||||||
})
|
})
|
||||||
.then(console.log, console.error, setResult);
|
.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);
|
|
||||||
};
|
|
||||||
|
26
src/templates/emails/coverMe.hbs
Normal file
26
src/templates/emails/coverMe.hbs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{{!--Subject: ССС: Нужен е заместник --}}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Търси се зместник:
|
||||||
|
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
|
||||||
|
</h3>
|
||||||
|
<p>Здравей {{firstName}},</p>
|
||||||
|
<p>{{prefix}} {{user.firstName}} {{user.lastName}} търси заместник.</p>
|
||||||
|
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
||||||
|
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||||
|
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
||||||
|
{{!-- Ти, той/тя и останалите участници в смяната ще
|
||||||
|
получат имейл за промяната. Твоята помощ е много ценна. --}}
|
||||||
|
</p>
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<a href="{{acceptUrl}}"
|
||||||
|
target="_blank"
|
||||||
|
style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; display: inline-block; border-radius: 5px;">Ще
|
||||||
|
поема смяната</a>
|
||||||
|
</p>
|
||||||
|
{{!-- <p>Thank you very much for considering my request.</p>
|
||||||
|
<p>Best regards,<br>{{name}}</p> --}}
|
||||||
|
</section>
|
||||||
|
<footer style="margin-top: 20px; text-align: center;">
|
||||||
|
<p>Изпратено до {{firstName}} {{lastName}} {{email}} {{sentDate}}</p>
|
||||||
|
</footer>
|
19
src/templates/emails/coverMeAccepted.hbs
Normal file
19
src/templates/emails/coverMeAccepted.hbs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{{!-- Subject: ССС: Промени в твоята смяна --}}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
|
||||||
|
<p>Здравейте {{firstName}}, </p>
|
||||||
|
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
|
||||||
|
<p>Новаия списък с участници за тази смяна е:</p>
|
||||||
|
<ul>
|
||||||
|
{{#each newPubs}}
|
||||||
|
<li>{{this.name}} - {{this.phone}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{!-- <footer class="footer">
|
||||||
|
Изпратено на: {{sentDate}}
|
||||||
|
</footer> --}}
|
24
src/templates/emails/example.hbs
Normal file
24
src/templates/emails/example.hbs
Normal file
@ -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. --}}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
|
||||||
|
<p>Здравейте,</p>
|
||||||
|
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
|
||||||
|
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
||||||
|
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||||
|
<p>С натискането на бутона по-долу можете да премете да го замествате. Вие, той/тя и останалите участници в смяната
|
||||||
|
ще бъдат уведумени чрез имейл за промяната. Вашата помощ е много ценна.</p>
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<a href="{{acceptUrl}}"
|
||||||
|
target="_blank"
|
||||||
|
style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; display: inline-block; border-radius: 5px;">Ще
|
||||||
|
поема смяната</a>
|
||||||
|
</p>
|
||||||
|
{{!-- <p>Thank you very much for considering my request.</p>
|
||||||
|
<p>Best regards,<br>{{name}}</p> --}}
|
||||||
|
</section>
|
||||||
|
<footer style="margin-top: 20px; text-align: center;">
|
||||||
|
<p>Изпратено на: {{sentDate}}</p>
|
||||||
|
</footer>
|
25
src/templates/emails/main.hbs
Normal file
25
src/templates/emails/main.hbs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ССС известия</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
||||||
|
<h2>Cпециално Свидетелстване София</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main style="margin: 20px;">
|
||||||
|
{{{body}}}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
||||||
|
© 2024 ССС. All rights reserved.
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
14
src/templates/emails/newShifts.hbs
Normal file
14
src/templates/emails/newShifts.hbs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{{!-- Subject: ССС: Нови назначени смени--}}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Здравейте, {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||||
|
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
||||||
|
<p>За месец {{month}} имате следните смени:</p>
|
||||||
|
<div>
|
||||||
|
{{{shifts}}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
Изпратено на: {{sentDate}}
|
||||||
|
</footer>
|
Reference in New Issue
Block a user