Merge commit '3be5e710c6b5512cce5df3d967065f406dcdbfed' into feature-myshedule
This commit is contained in:
26
.env
26
.env
@ -1,22 +1,13 @@
|
|||||||
|
|
||||||
#NODE_TLS_REJECT_UNAUTHORIZED='0'
|
#NODE_TLS_REJECT_UNAUTHORIZED='0'
|
||||||
SSL_ENABLED=false
|
# HOST=localhost
|
||||||
NEXT_PUBLIC_PROTOCOL=https
|
# PORT=3003
|
||||||
NEXT_PUBLIC_HOST=localhost
|
# NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003
|
||||||
NEXT_PUBLIC_PORT=3003
|
|
||||||
NEXTAUTH_URL=https://localhost:3003
|
|
||||||
# NEXTAUTH_URL_INTERNAL=http://127.0.0.1:3003
|
|
||||||
|
|
||||||
# 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
|
||||||
@ -28,10 +19,13 @@ AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret v
|
|||||||
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
||||||
|
|
||||||
|
|
||||||
APPLE_ID=
|
APPLE_ID=com.mwhitnessing.sofia
|
||||||
APPLE_TEAM_ID=
|
APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
||||||
|
# to generate
|
||||||
|
APPLE_TEAM_ID=XC57P9SXDK
|
||||||
|
APPLE_KEY_ID=TB3V355G5Y
|
||||||
APPLE_PRIVATE_KEY=
|
APPLE_PRIVATE_KEY=
|
||||||
APPLE_KEY_ID=
|
|
||||||
|
|
||||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
||||||
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
||||||
|
37
.env.demo
37
.env.demo
@ -1,37 +0,0 @@
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED='0'
|
|
||||||
# DATABASE_URL="file:./src/data/dev.db"
|
|
||||||
# DATABASE_URL="mysql://root:Zelen0ku4e@192.168.0.10:3306/cart"
|
|
||||||
|
|
||||||
NEXT_PUBLIC_PORT=
|
|
||||||
# NEXT_PUBLIC_NEXTAUTH_URL=https://cart.d-popov.com
|
|
||||||
NEXT_PUBLIC_PROTOCOL=https
|
|
||||||
NEXT_PUBLIC_HOST=cart.d-popov.com
|
|
||||||
NEXTAUTH_URL=https://cart.d-popov.com
|
|
||||||
# NEXTAUTH_URL= https://demo.mwhitnessing.com
|
|
||||||
|
|
||||||
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
|
||||||
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
|
||||||
DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart_demo
|
|
||||||
|
|
||||||
APPLE_ID=
|
|
||||||
APPLE_TEAM_ID=
|
|
||||||
APPLE_PRIVATE_KEY=
|
|
||||||
APPLE_KEY_ID=
|
|
||||||
|
|
||||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
|
||||||
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
|
||||||
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
|
|
||||||
|
|
||||||
FACEBOOK_ID=
|
|
||||||
FACEBOOK_SECRET=
|
|
||||||
|
|
||||||
GITHUB_ID=
|
|
||||||
GITHUB_SECRET=
|
|
||||||
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
|
||||||
# GOOGLE_SECRET=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
|
|
||||||
|
|
||||||
TWITTER_ID=
|
|
||||||
TWITTER_SECRET=
|
|
||||||
|
|
||||||
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
|
||||||
EMAIL_FROM=noreply@example.com
|
|
@ -1,15 +1,11 @@
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert
|
# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert
|
||||||
|
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
|
||||||
|
|
||||||
NEXT_PUBLIC_PROTOCOL=https
|
|
||||||
NEXT_PUBLIC_HOST=localhost
|
|
||||||
NEXT_PUBLIC_PORT=3003
|
|
||||||
NEXTAUTH_URL=https://localhost:3003
|
|
||||||
|
|
||||||
SSL_ENABLED=true
|
|
||||||
TELEGRAM_BOT=true
|
|
||||||
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
|
@ -1,9 +0,0 @@
|
|||||||
NEXT_PUBLIC_PROTOCOL=https
|
|
||||||
NEXT_PUBLIC_PORT=
|
|
||||||
NEXT_PUBLIC_HOST=staging.mwhitnessing.com
|
|
||||||
NEXTAUTH_URL= https://staging.mwhitnessing.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-staging:3306/jwpwsofia
|
|
@ -1,9 +1,9 @@
|
|||||||
NEXT_PUBLIC_PROTOCOL=https
|
PORT=
|
||||||
NEXT_PUBLIC_PORT=
|
HOST=sofia.mwitnessing.com
|
||||||
NEXT_PUBLIC_HOST=sofia.mwhitnessing.com
|
PROTOCOL=http # we're behind a reverse proxy. SSL is handled by the proxy
|
||||||
NEXTAUTH_URL= https://sofia.mwhitnessing.com
|
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
|
22
.env.test
22
.env.test
@ -1,13 +1,14 @@
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED='0'
|
NODE_ENV=test
|
||||||
|
|
||||||
NEXT_PUBLIC_PORT=5001
|
PROTOCOL=http
|
||||||
NEXT_PUBLIC_PROTOCOL=https
|
HOST=staging.mwitnessing.com
|
||||||
NEXT_PUBLIC_HOST=cart.d-popov.com
|
PORT=
|
||||||
NEXTAUTH_URL=https://cart.d-popov.com
|
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=ed8a9681efc414df89dfd03cd188ed58
|
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||||
DATABASE_URL=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||||
|
DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
|
||||||
|
|
||||||
APPLE_ID=
|
APPLE_ID=
|
||||||
APPLE_TEAM_ID=
|
APPLE_TEAM_ID=
|
||||||
@ -23,14 +24,11 @@ FACEBOOK_SECRET=
|
|||||||
|
|
||||||
GITHUB_ID=
|
GITHUB_ID=
|
||||||
GITHUB_SECRET=
|
GITHUB_SECRET=
|
||||||
GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
||||||
GOOGLE_SECRET=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
|
# GOOGLE_SECRET=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
|
||||||
|
|
||||||
TWITTER_ID=
|
TWITTER_ID=
|
||||||
TWITTER_SECRET=
|
TWITTER_SECRET=
|
||||||
|
|
||||||
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
||||||
EMAIL_FROM=noreply@example.com
|
EMAIL_FROM=noreply@example.com
|
||||||
|
|
||||||
GMAIL_EMAIL_USERNAME=
|
|
||||||
GMAIL_EMAIL_APP_PASS=
|
|
||||||
|
18
.vscode/launch.json
vendored
18
.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-env",
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,7 +45,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"command": "conda activate node && npm run debug-env",
|
"command": "conda activate node && npm run debug",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Run conda npm TEST",
|
"name": "Run conda npm TEST",
|
||||||
|
0
DESIGN/readme.md
Normal file
0
DESIGN/readme.md
Normal file
6
_deploy/appleKey.p8
Normal file
6
_deploy/appleKey.p8
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/
|
||||||
|
wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ
|
||||||
|
c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6
|
||||||
|
/KyhaFLl
|
||||||
|
-----END PRIVATE KEY-----
|
@ -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
|
||||||
|
38
_deploy/deoloy.azure.demo.yml
Normal file
38
_deploy/deoloy.azure.demo.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
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:
|
||||||
|
- APP_ENV=test
|
||||||
|
- NODE_ENV=test
|
||||||
|
- TZ=Europe/Sofia
|
||||||
|
- 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
|
||||||
|
- GIT_PASSWORD=L3Kr2R438u4F7
|
||||||
|
command: sh -c " cd /app && npm install && npx next build && npm run nodeenv; tail -f /dev/null"
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- infrastructure_default
|
||||||
|
mariadb:
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
hostname: mariadb-demo
|
||||||
|
image: mysql:latest #mariadb:10.4
|
||||||
|
volumes:
|
||||||
|
- /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:
|
||||||
|
external: true
|
@ -1,57 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
|
||||||
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-staging/app/public/content/uploads/:/app/public/content/uploads
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=prod_staging
|
|
||||||
- TZ=Europe/Sofia
|
|
||||||
- DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb-staging:3306/jwpwsofia
|
|
||||||
- UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git
|
|
||||||
- GIT_BRANCH=main
|
|
||||||
- GIT_USERNAME=deploy
|
|
||||||
- GIT_PASSWORD=L3Kr2R438u4F7
|
|
||||||
command: sh -c " cd /app && npm install && npm run nodeenv; tail -f /dev/null"
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
restart: always
|
|
||||||
# ports:
|
|
||||||
# - "3001:3000"
|
|
||||||
networks:
|
|
||||||
- infrastructure_default
|
|
||||||
mariadb:
|
|
||||||
hostname: mariadb-staging
|
|
||||||
image: mariadb:latest #mariadb:10.4
|
|
||||||
volumes:
|
|
||||||
- /mnt/docker_volumes/pw-staging/data/mysql:/var/lib/mysql
|
|
||||||
environment:
|
|
||||||
MARIADB_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262
|
|
||||||
MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262
|
|
||||||
MYSQL_DATABASE: jwpwsofia
|
|
||||||
MYSQL_USER: jwpwsofia
|
|
||||||
MYSQL_PASSWORD: dwxhns9p9vp248V39xJyRthUsZ2gR9
|
|
||||||
#command: ["mysqld", "--max-connections=1000", "--sql-mode=ALLOW_INVALID_DATES,ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO,HIGH_NOT_PRECEDENCE,IGNORE_SPACE,NO_AUTO_CREATE_USER,NO_AUTO_VALUE_ON_ZERO,NO_BACKSLASH_ESCAPES,NO_DIR_IN_CREATE,NO_ENGINE_SUBSTITUTION,NO_FIELD_OPTIONS,NO_KEY_OPTIONS,NO_TABLE_OPTIONS,NO_UNSIGNED_SUBTRACTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY,PIPES_AS_CONCAT,REAL_AS_FLOAT,STRICT_ALL_TABLES,STRICT_TRANS_TABLES,ANSI,DB2,MAXDB,MSSQL,MYSQL323,MYSQL40,ORACLE,POSTGRESQL,TRADITIONAL", "--wait-timeout=28800"]
|
|
||||||
networks:
|
|
||||||
- infrastructure_default
|
|
||||||
postgres:
|
|
||||||
hostname: postgres
|
|
||||||
image: postgres
|
|
||||||
restart: always
|
|
||||||
# set shared memory limit when using docker-compose
|
|
||||||
shm_size: 128mb
|
|
||||||
# or set shared memory limit when deploy via swarm stack
|
|
||||||
#volumes:
|
|
||||||
# - type: tmpfs
|
|
||||||
# target: /dev/shm
|
|
||||||
# tmpfs:
|
|
||||||
# size: 134217728 # 128*2^20 bytes = 128Mb
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- infrastructure_default
|
|
||||||
networks:
|
|
||||||
infrastructure_default:
|
|
||||||
external: true
|
|
@ -1,8 +1,10 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
nextjs-app: # https://sofia.mwhitnessing.com/
|
nextjs-app: # https://sofia.mwitnessing.com/
|
||||||
hostname: jwpw-app # jwpw-nextjs-app-1
|
hostname: jwpw-app # jwpw-nextjs-app-1
|
||||||
image: docker.d-popov.com/jwpw:latest
|
image: docker.d-popov.com/jwpw:latest
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
#ports:
|
#ports:
|
||||||
# - "3000:3000"
|
# - "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
@ -10,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
|
||||||
@ -30,7 +32,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /mnt/docker_volumes/pw/data/mysql:/var/lib/mysql
|
- /mnt/docker_volumes/pw/data/mysql:/var/lib/mysql
|
||||||
environment:
|
environment:
|
||||||
MARIADB_ROOT_PASSWORD: dwxhns9p9vp248V39xJyRthUsZ2gR9
|
# MARIADB_ROOT_PASSWORD: dwxhns9p9vp248V39xJyRthUsZ2gR9
|
||||||
MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262
|
MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262
|
||||||
MYSQL_DATABASE: jwpwsofia
|
MYSQL_DATABASE: jwpwsofia
|
||||||
MYSQL_USER: jwpwsofia
|
MYSQL_USER: jwpwsofia
|
||||||
@ -38,7 +40,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- infrastructure_default
|
- infrastructure_default
|
||||||
|
|
||||||
mariadb_backup:
|
mariadb_backup:
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
volumes:
|
volumes:
|
||||||
@ -51,11 +52,11 @@ services:
|
|||||||
MYSQL_HOST: mariadb
|
MYSQL_HOST: mariadb
|
||||||
# GOOGLE_DRIVE_FOLDER_ID: your_google_drive_folder_id
|
# GOOGLE_DRIVE_FOLDER_ID: your_google_drive_folder_id
|
||||||
entrypoint: /bin/sh -c
|
entrypoint: /bin/sh -c
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- infrastructure_default
|
- infrastructure_default
|
||||||
command: |
|
command: |
|
||||||
"apk add --no-cache mysql-client curl && \
|
"apk update && \
|
||||||
|
apk add --no-cache mariadb-client mariadb-connector-c && \
|
||||||
echo '0 2 * * * mysqldump -h $$MYSQL_HOST -P 3306 -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql' > /etc/crontabs/root && \
|
echo '0 2 * * * mysqldump -h $$MYSQL_HOST -P 3306 -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql' > /etc/crontabs/root && \
|
||||||
crond -f -d 8"
|
crond -f -d 8"
|
||||||
# wget -q https://github.com/prasmussen/gdrive/releases/download/2.1.0/gdrive-linux-x64 -O /usr/bin/gdrive && \
|
# wget -q https://github.com/prasmussen/gdrive/releases/download/2.1.0/gdrive-linux-x64 -O /usr/bin/gdrive && \
|
@ -10,7 +10,7 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then
|
|||||||
mkdir /tmp/clone
|
mkdir /tmp/clone
|
||||||
|
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwhitnessing.git /tmp/clone || exit 1
|
git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwitnessing.git /tmp/clone || exit 1
|
||||||
|
|
||||||
# Synchronize all files except package.json and package-lock.json to /app
|
# Synchronize all files except package.json and package-lock.json to /app
|
||||||
rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files"
|
rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files"
|
||||||
|
@ -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
|
||||||
@ -15,7 +15,7 @@ services:
|
|||||||
stdin_open: true
|
stdin_open: true
|
||||||
mariadb:
|
mariadb:
|
||||||
hostname: mariadb
|
hostname: mariadb
|
||||||
image: mariadb #bitnami/mariadb:latest #mariadb:10.4
|
image: mariadb #bitnami/mariadb:latest #mariadb:10.4
|
||||||
environment:
|
environment:
|
||||||
MARIADB_ROOT_PASSWORD: Pw62L$3332JH
|
MARIADB_ROOT_PASSWORD: Pw62L$3332JH
|
||||||
MYSQL_ROOT_PASSWORD: Pw62L$3332JH
|
MYSQL_ROOT_PASSWORD: Pw62L$3332JH
|
||||||
@ -24,4 +24,20 @@ services:
|
|||||||
MYSQL_PASSWORD: o74x642Rc8
|
MYSQL_PASSWORD: o74x642Rc8
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- mysql_default
|
- mysql_default
|
||||||
|
postgres:
|
||||||
|
deploy:
|
||||||
|
replicas: 0
|
||||||
|
hostname: postgres
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
# set shared memory limit when using docker-compose
|
||||||
|
shm_size: 128mb
|
||||||
|
# or set shared memory limit when deploy via swarm stack
|
||||||
|
#volumes:
|
||||||
|
# - type: tmpfs
|
||||||
|
# target: /dev/shm
|
||||||
|
# tmpfs:
|
||||||
|
# size: 134217728 # 128*2^20 bytes = 128Mb
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262
|
@ -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"
|
||||||
|
78
_deploy/setupAppleId.mjs
Normal file
78
_deploy/setupAppleId.mjs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/node
|
||||||
|
|
||||||
|
import { SignJWT } from "jose"
|
||||||
|
import { createPrivateKey } from "crypto"
|
||||||
|
|
||||||
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||||
|
console.log(`
|
||||||
|
Creates a JWT from the components found at Apple.
|
||||||
|
By default, the JWT has a 6 months expiry date.
|
||||||
|
Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
|
||||||
|
APPLE_ID=com.mwhitnessing.sofia
|
||||||
|
APPLE_TEAM_ID=XC57P9SXDK
|
||||||
|
APPLE_KEY_ID=TB3V355G5Y
|
||||||
|
APPLE_KEY
|
||||||
|
|
||||||
|
node setupAppleId.mjs --kid YOUR_KEY_ID --iss YOUR_TEAM_ID --private_key "$(cat key.p8)" --sub YOUR_CLIENT_ID --expires_in 15778800
|
||||||
|
node setupAppleId.mjs --kid TB3V355G5Y --iss XC57P9SXDK --sub com.mwhitnessing.sofia --private_key "$(cat appleKey.p8)"
|
||||||
|
|
||||||
|
>>Apple client secret generated. Valid until: Tue Oct 01 2024 00:05:43 GMT+0300 (Eastern European Summer Time)
|
||||||
|
|
||||||
|
eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
||||||
|
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help Print this help message
|
||||||
|
--kid, --key_id The key id of the private key
|
||||||
|
--iss, --team_id The Apple team ID
|
||||||
|
--private_key The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----)
|
||||||
|
--sub, --client_id The client id to use in the JWT.
|
||||||
|
--expires_in Number of seconds from now when the JWT should expire. Defaults to 6 months.
|
||||||
|
--exp Future date in seconds when the JWT expires
|
||||||
|
`)
|
||||||
|
} else {
|
||||||
|
const args = process.argv.slice(2).reduce((acc, arg, i) => {
|
||||||
|
if (arg.match(/^--\w/)) {
|
||||||
|
const key = arg.replace(/^--/, "").toLowerCase()
|
||||||
|
acc[key] = process.argv[i + 3]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const {
|
||||||
|
team_id,
|
||||||
|
iss = team_id,
|
||||||
|
|
||||||
|
private_key,
|
||||||
|
|
||||||
|
client_id,
|
||||||
|
sub = client_id,
|
||||||
|
|
||||||
|
key_id,
|
||||||
|
kid = key_id,
|
||||||
|
|
||||||
|
expires_in = 86400 * 180,
|
||||||
|
exp = Math.ceil(Date.now() / 1000) + expires_in,
|
||||||
|
} = args
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long is the secret valid in seconds.
|
||||||
|
* @default 15780000
|
||||||
|
*/
|
||||||
|
const expiresAt = Math.ceil(Date.now() / 1000) + expires_in
|
||||||
|
const expirationTime = exp ?? expiresAt
|
||||||
|
console.log(`
|
||||||
|
Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)}
|
||||||
|
|
||||||
|
${await new SignJWT({})
|
||||||
|
.setAudience("https://appleid.apple.com")
|
||||||
|
.setIssuer(iss)
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(expirationTime)
|
||||||
|
.setSubject(sub)
|
||||||
|
.setProtectedHeader({ alg: "ES256", kid })
|
||||||
|
.sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
|
||||||
|
}
|
15
_doc/ToDo.md
15
_doc/ToDo.md
@ -187,3 +187,18 @@ fix availability repeat checks
|
|||||||
sometimes delete from mycalendar fails
|
sometimes delete from mycalendar fails
|
||||||
saturday shifts start at 12:00 / dymamic
|
saturday shifts start at 12:00 / dymamic
|
||||||
|
|
||||||
|
-------------------------------
|
||||||
|
Add availability type UNAVAILABLE/ AWAY (like Estelle, Rick, Me)
|
||||||
|
|
||||||
|
why "Александра Чернъшова" seems available every shift thursdays?
|
||||||
|
fix Time ZONE (currently Z, but it leads to shift when the DST changes ( winter entries are shifter in summer))
|
||||||
|
защо Марсел Клайнер е червен четв 11 април? - има предпочитания и е в номата
|
||||||
|
fix repeating availabilities - Tanq kolcjanova only blue first thursday
|
||||||
|
add assignment in calendar planner
|
||||||
|
fix database
|
||||||
|
|
||||||
|
--
|
||||||
|
emails
|
||||||
|
mobile apps
|
||||||
|
apple login
|
||||||
|
разрешителни - upload
|
||||||
|
67
_doc/apple-gen-secret.mjs
Normal file
67
_doc/apple-gen-secret.mjs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/node
|
||||||
|
# https://gist.githubusercontent.com/balazsorban44/09613175e7b37ec03f676dcefb7be5eb/raw/b0d31aa0c7f58e0088fdf59ec30cad1415a3475b/apple-gen-secret.mjs
|
||||||
|
|
||||||
|
import { SignJWT } from "jose"
|
||||||
|
import { createPrivateKey } from "crypto"
|
||||||
|
|
||||||
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||||
|
console.log(`
|
||||||
|
Creates a JWT from the components found at Apple.
|
||||||
|
By default, the JWT has a 6 months expiry date.
|
||||||
|
Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help Print this help message
|
||||||
|
--kid, --key_id The key id of the private key
|
||||||
|
--iss, --team_id The Apple team ID
|
||||||
|
--private_key The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----)
|
||||||
|
--sub, --client_id The client id to use in the JWT.
|
||||||
|
--expires_in Number of seconds from now when the JWT should expire. Defaults to 6 months.
|
||||||
|
--exp Future date in seconds when the JWT expires
|
||||||
|
`)
|
||||||
|
} else {
|
||||||
|
const args = process.argv.slice(2).reduce((acc, arg, i) => {
|
||||||
|
if (arg.match(/^--\w/)) {
|
||||||
|
const key = arg.replace(/^--/, "").toLowerCase()
|
||||||
|
acc[key] = process.argv[i + 3]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const {
|
||||||
|
team_id,
|
||||||
|
iss = team_id,
|
||||||
|
|
||||||
|
private_key,
|
||||||
|
|
||||||
|
client_id,
|
||||||
|
sub = client_id,
|
||||||
|
|
||||||
|
key_id,
|
||||||
|
kid = key_id,
|
||||||
|
|
||||||
|
expires_in = 86400 * 180,
|
||||||
|
exp = Math.ceil(Date.now() / 1000) + expires_in,
|
||||||
|
} = args
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long is the secret valid in seconds.
|
||||||
|
* @default 15780000
|
||||||
|
*/
|
||||||
|
const expiresAt = Math.ceil(Date.now() / 1000) + expires_in
|
||||||
|
const expirationTime = exp ?? expiresAt
|
||||||
|
console.log(`
|
||||||
|
Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)}
|
||||||
|
|
||||||
|
${await new SignJWT({})
|
||||||
|
.setAudience("https://appleid.apple.com")
|
||||||
|
.setIssuer(iss)
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(expirationTime)
|
||||||
|
.setSubject(sub)
|
||||||
|
.setProtectedHeader({ alg: "ES256", kid })
|
||||||
|
.sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
|
||||||
|
}
|
@ -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
|
||||||
@ -153,8 +158,13 @@ Remove-Item package-lock.json
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
|
||||||
# -- mysql
|
# ---------------------------------- mysql ----------------------------------- #
|
||||||
# fix
|
#backup: (--no-data to skip data)
|
||||||
|
mysqldump -h mariadb -P 3306 -ujwpwsofia -p"dwxhns9p9vp248V39xJyRthUsZ2gR9" jwpwsofia --skip-add-locks > /backup/manual-$(date +\%Y-\%m-\%d-\%H\%M\%S)-$MYSQL_DATABASE.sql
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# fix++
|
||||||
mysql -u root -pi4966cWBtP3xJ7BLsbsgo93C8Q5262
|
mysql -u root -pi4966cWBtP3xJ7BLsbsgo93C8Q5262
|
||||||
--
|
--
|
||||||
mysqld_safe --skip-grant-tables &
|
mysqld_safe --skip-grant-tables &
|
||||||
@ -164,8 +174,15 @@ SET PASSWORD FOR 'root'@'localhost' = PASSWORD('i4966cWBtP3xJ7BLsbsgo93C8Q5262')
|
|||||||
GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION;
|
GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION;
|
||||||
GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.3' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION;
|
GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.3' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION;
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION;
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
exit;
|
exit;
|
||||||
|
ALTER USER 'jwpwsofia'@'172.22.0.%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9';
|
||||||
|
--if error (does not exist)
|
||||||
|
CREATE USER 'jwpwsofia'@'172.22.0.%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9';
|
||||||
|
GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.%' WITH GRANT OPTION;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#Install depcheck:
|
#Install depcheck:
|
||||||
@ -184,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:
|
||||||
|
@ -40,7 +40,7 @@ class ExampleForm extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [item, set] = useState({
|
const [item, set] = useState({
|
||||||
isactive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -63,7 +63,7 @@ class ExampleForm extends React.Component {
|
|||||||
|
|
||||||
|
|
||||||
handleChange = ({ target }) => {
|
handleChange = ({ target }) => {
|
||||||
if (target.name === "isactive") {
|
if (target.name === "isActive") {
|
||||||
set({ ...item, [target.name]: target.checked });
|
set({ ...item, [target.name]: target.checked });
|
||||||
} else if (target.name === "age") {
|
} else if (target.name === "age") {
|
||||||
set({ ...item, [target.name]: parseInt(target.value) });
|
set({ ...item, [target.name]: parseInt(target.value) });
|
||||||
@ -100,8 +100,8 @@ class ExampleForm extends React.Component {
|
|||||||
<h3>{router.query?.id ? "Редактирай" : "Създай"} Item </h3>
|
<h3>{router.query?.id ? "Редактирай" : "Създай"} Item </h3>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input className="checkbox" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={item.isactive} autoComplete="off" />
|
<input className="checkbox" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={item.isActive} autoComplete="off" />
|
||||||
<label className="label" htmlFor="isactive">
|
<label className="label" htmlFor="isActive">
|
||||||
Is Active</label>
|
Is Active</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
43
components/Modal.tsx
Normal file
43
components/Modal.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import common from '../src/helpers/common'; // Ensure this path is correct
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
forDate: Date;
|
||||||
|
useFilterDate: boolean;
|
||||||
|
onUseFilterDateChange: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Modal({ children, isOpen, onClose, forDate, useFilterDate, onUseFilterDateChange }: ModalProps) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
const isValidDate = forDate instanceof Date && !isNaN(forDate.getTime());
|
||||||
|
console.log("forDate", forDate, isValidDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white p-4 rounded-md shadow-lg modal-content">
|
||||||
|
{isValidDate && (
|
||||||
|
<h2 className="text-xl font-bold mb-2">
|
||||||
|
<label className="cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useFilterDate}
|
||||||
|
onChange={(e) => onUseFilterDateChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
{` на разположение ${common.getDateFormated(forDate)} или ${common.getDayOfWeekName(forDate)}`}
|
||||||
|
</label>
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
<button type="button" onClick={onClose} className="mt-4 text-red-500">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="fixed inset-0 bg-black opacity-50 modal-overlay" onClick={onClose}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
@ -1,5 +1,5 @@
|
|||||||
import axiosInstance from '../../src/axiosSecure';
|
import axiosInstance from '../../src/axiosSecure';
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback, use } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
@ -9,6 +9,8 @@ import bg from 'date-fns/locale/bg';
|
|||||||
import { bgBG } from '../x-date-pickers/locales/bgBG';
|
import { bgBG } from '../x-date-pickers/locales/bgBG';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
|
//todo import Availability type from prisma schema
|
||||||
|
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
const config = await import('../../config.json');
|
const config = await import('../../config.json');
|
||||||
@ -23,12 +25,12 @@ 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));
|
||||||
const [doRepeat, setDoRepeat] = useState(false);
|
|
||||||
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
|
||||||
const [repeatUntil, setRepeatUntil] = useState(null);
|
|
||||||
const [canUpdate, setCanUpdate] = useState(true);
|
const [canUpdate, setCanUpdate] = useState(true);
|
||||||
|
|
||||||
const [timeSlots, setTimeSlots] = useState([]);
|
const [timeSlots, setTimeSlots] = useState([]);
|
||||||
@ -39,13 +41,17 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
dayOfMonth: null,
|
dayOfMonth: null,
|
||||||
// startTime: "08:00",
|
// startTime: "08:00",
|
||||||
// endTime: "20:00",
|
// endTime: "20:00",
|
||||||
isactive: true,
|
isActive: true,
|
||||||
repeatWeekly: false,
|
repeatWeekly: false,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
isFirst: false,
|
isFirst: false,
|
||||||
isLast: false,
|
isLast: false,
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
const [doRepeat, setDoRepeat] = useState(existingItems && existingItems.length > 0 ? existingItems[0].repeatWeekly : false);
|
||||||
|
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
||||||
|
const [repeatUntil, setRepeatUntil] = useState(null);
|
||||||
|
|
||||||
const [isInline, setInline] = useState(inline || false);
|
const [isInline, setInline] = useState(inline || false);
|
||||||
const [config, setConfig] = useState(null);
|
const [config, setConfig] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -56,30 +62,28 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Define the minimum and maximum times
|
// Define the minimum and maximum times
|
||||||
const minTime = new Date();
|
const minTime = new Date();
|
||||||
minTime.setHours(8, 0, 0, 0); // 8:00 AM
|
minTime.setHours(8, 0, 0, 0); // 8:00 AM
|
||||||
const maxTime = new Date();
|
const maxTime = new Date();
|
||||||
maxTime.setHours(20, 0, 0, 0); // 8:00 PM
|
maxTime.setHours(20, 0, 0, 0); // 8:00 PM
|
||||||
|
|
||||||
|
const fetchItemFromDB = async () => {
|
||||||
|
const id = parseInt(router.query.id);
|
||||||
|
if (existingItems.length == 0 && id) {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
|
||||||
|
setAvailabilities([response.data]);
|
||||||
|
setEditMode(true);
|
||||||
|
setDoRepeat(response.data.repeatWeekly);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Error fetching availability data.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchItemFromDB = async () => {
|
|
||||||
const id = parseInt(router.query.id);
|
|
||||||
if (existingItems.length == 0 && id) {
|
|
||||||
try {
|
|
||||||
const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
|
|
||||||
setAvailabilities([response.data]);
|
|
||||||
setEditMode(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("Error fetching availability data.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchItemFromDB();
|
fetchItemFromDB();
|
||||||
}, [router.query.id]);
|
}, [router.query.id]);
|
||||||
|
|
||||||
@ -88,32 +92,34 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots);
|
const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots);
|
||||||
|
let avs = availabilities.filter(av => av.type !== "assignment");
|
||||||
// Determine if we need to delete and recreate, or just update
|
// Determine if we need to delete and recreate, or just update
|
||||||
const shouldRecreate = availabilities.length !== groupedTimeSlots.length || availabilities.some(av => !av.id);
|
let shouldRecreate = avs.length > 0 && avs.length !== groupedTimeSlots.length || avs.some(av => !av.id);
|
||||||
|
shouldRecreate = shouldRecreate || (avs.length == 0 && availabilities.length > 0);
|
||||||
|
//create availability if we open a form with assignment without availability
|
||||||
|
|
||||||
if (shouldRecreate) {
|
if (shouldRecreate) {
|
||||||
// Delete existing availabilities if they have an ID
|
// Delete existing availabilities if they have an ID
|
||||||
console.log("Recreating availabilities");
|
console.log("Recreating availabilities");
|
||||||
await Promise.all(availabilities.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`)));
|
await Promise.all(avs.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`)));
|
||||||
|
|
||||||
// Create new availabilities
|
// Create new availabilities
|
||||||
const createdAvailabilities = await Promise.all(groupedTimeSlots.map(async group => {
|
avs = await Promise.all(groupedTimeSlots.map(async group => {
|
||||||
const newAvailability = createAvailabilityFromGroup(group, publisher.id);
|
const newAvailability = createAvailabilityFromGroup(group, publisher.id);
|
||||||
const response = await axiosInstance.post(urls.apiUrl, newAvailability);
|
const response = await axiosInstance.post(urls.apiUrl, newAvailability);
|
||||||
return response.data; // Assuming the new availability is returned
|
return response.data; // Assuming the new availability is returned
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setAvailabilities(createdAvailabilities);
|
setAvailabilities(avs);
|
||||||
} else {
|
} else {
|
||||||
// Update existing availabilities
|
// Update existing availabilities
|
||||||
console.log("Updating existing availabilities");
|
console.log("Updating existing availabilities");
|
||||||
const updatedAvailabilities = await Promise.all(availabilities.map(async (availability, index) => {
|
avs = await Promise.all(avs.map(async (availability, index) => {
|
||||||
const group = groupedTimeSlots[index];
|
const group = groupedTimeSlots[index];
|
||||||
const id = availability.id;
|
const id = availability.id;
|
||||||
const updatedAvailability = updateAvailabilityFromGroup(availability, group);
|
const updatedAvailability = updateAvailabilityFromGroup(availability, group);
|
||||||
delete updatedAvailability.id;
|
delete updatedAvailability.id;
|
||||||
delete updatedAvailability.type;
|
//delete updatedAvailability.type;
|
||||||
delete updatedAvailability.publisherId;
|
delete updatedAvailability.publisherId;
|
||||||
delete updatedAvailability.title;
|
delete updatedAvailability.title;
|
||||||
delete updatedAvailability.date;
|
delete updatedAvailability.date;
|
||||||
@ -122,7 +128,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
return updatedAvailability;
|
return updatedAvailability;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setAvailabilities(updatedAvailabilities);
|
setAvailabilities(avs);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCompletion({ updated: true });
|
handleCompletion({ updated: true });
|
||||||
@ -172,54 +178,123 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
return groupedIntervals;
|
return groupedIntervals;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport;
|
// // const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport;
|
||||||
// const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport;
|
// // const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport;
|
||||||
function createAvailabilityFromGroup(group) {
|
// function createAvailabilityFromGroup(group) {
|
||||||
let startTime = new Date(day);
|
// let startTime = new Date(day);
|
||||||
|
// startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0);
|
||||||
|
|
||||||
|
// let endTime = new Date(day);
|
||||||
|
// endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0);
|
||||||
|
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// name: common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime),
|
||||||
|
// publisherId: publisher.id,
|
||||||
|
// startTime: startTime,
|
||||||
|
// endTime: endTime,
|
||||||
|
// isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport,
|
||||||
|
// isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport,
|
||||||
|
// dayofweek: common.getDayOfWeekNameEnEnumForDate(day.getDay()),
|
||||||
|
// repeatWeekly: doRepeat,
|
||||||
|
// dayOfMonth: doRepeat ? null : startTime.getDate(),
|
||||||
|
// endDate: doRepeat ? repeatUntil : null,
|
||||||
|
// dateOfEntry: new Date(),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function updateAvailabilityFromGroup(availability, group) {
|
||||||
|
// availability.startTime.setTime(group[0].startTime);
|
||||||
|
// availability.endTime.setTime(group[group.length - 1].endTime);
|
||||||
|
// availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
|
||||||
|
|
||||||
|
// availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
|
||||||
|
// availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport;
|
||||||
|
|
||||||
|
// delete availability.weekOfMonth;
|
||||||
|
// if (doRepeat) {
|
||||||
|
// availability.repeatWeekly = true;
|
||||||
|
// availability.dayOfMonth = null;
|
||||||
|
// availability.weekOfMonth = 0;
|
||||||
|
// availability.endDate = repeatUntil;
|
||||||
|
// } else {
|
||||||
|
// availability.repeatWeekly = false;
|
||||||
|
// availability.dayOfMonth = availability.startTime.getDate();
|
||||||
|
// availability.endDate = null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// availability.dateOfEntry = new Date();
|
||||||
|
// if (availability.parentAvailabilityId) {
|
||||||
|
// availability.parentAvailability = { connect: { id: parentAvailabilityId } };
|
||||||
|
// }
|
||||||
|
// delete availability.parentAvailabilityId;
|
||||||
|
|
||||||
|
// return availability;
|
||||||
|
// }
|
||||||
|
// Common function to set shared properties
|
||||||
|
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
||||||
|
let startTime = new Date(availability.startTime || day);
|
||||||
startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0);
|
startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0);
|
||||||
|
|
||||||
let endTime = new Date(day);
|
let endTime = new Date(availability.endTime || day);
|
||||||
endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0);
|
endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0);
|
||||||
|
|
||||||
|
availability.startTime = startTime;
|
||||||
return {
|
availability.endTime = endTime;
|
||||||
name: common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime),
|
availability.name = common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime);
|
||||||
publisherId: publisher.id,
|
|
||||||
startTime: startTime,
|
|
||||||
endTime: endTime,
|
|
||||||
isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport,
|
|
||||||
isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport,
|
|
||||||
dayofweek: common.getDayOfWeekNameEnEnum(day.getDay()),
|
|
||||||
repeatWeekly: doRepeat,
|
|
||||||
dayOfMonth: doRepeat ? null : startTime.getDate(),
|
|
||||||
endDate: doRepeat ? repeatUntil : null,
|
|
||||||
dateOfEntry: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAvailabilityFromGroup(availability, group) {
|
|
||||||
availability.startTime.setTime(group[0].startTime);
|
|
||||||
availability.endTime.setTime(group[group.length - 1].endTime);
|
|
||||||
availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
|
|
||||||
|
|
||||||
availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
|
availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
|
||||||
availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport;
|
availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport;
|
||||||
|
|
||||||
availability.repeatWeekly = doRepeat;
|
// Adjustments for repeating settings
|
||||||
availability.dayOfMonth = doRepeat ? null : availability.startTime.getDate();
|
if (doRepeat) {
|
||||||
availability.endDate = doRepeat ? repeatUntil : null;
|
availability.repeatWeekly = true;
|
||||||
|
availability.type = "Weekly"
|
||||||
|
availability.dayOfMonth = null;
|
||||||
|
availability.endDate = repeatUntil;
|
||||||
|
} else {
|
||||||
|
availability.type = "OneTime"
|
||||||
|
availability.repeatWeekly = false;
|
||||||
|
availability.dayOfMonth = startTime.getDate();
|
||||||
|
availability.endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
availability.dateOfEntry = new Date();
|
availability.dateOfEntry = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAvailabilityFromGroup(group) {
|
||||||
|
let availability = {
|
||||||
|
publisherId: publisher.id,
|
||||||
|
dayofweek: common.getDayOfWeekNameEnEnumForDate(day),
|
||||||
|
};
|
||||||
|
|
||||||
|
setSharedAvailabilityProperties(availability, group, timeSlots);
|
||||||
|
|
||||||
return availability;
|
return availability;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAvailabilityFromGroup(availability, group) {
|
||||||
|
setSharedAvailabilityProperties(availability, group, timeSlots);
|
||||||
|
|
||||||
|
delete availability.weekOfMonth;
|
||||||
|
if (doRepeat) {
|
||||||
|
availability.weekOfMonth = 0;
|
||||||
|
}
|
||||||
|
if (availability.parentAvailabilityId) {
|
||||||
|
availability.parentAvailability = { connect: { id: parentAvailabilityId } };
|
||||||
|
}
|
||||||
|
delete availability.parentAvailabilityId;
|
||||||
|
|
||||||
|
return availability;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleDelete = async (e) => {
|
const handleDelete = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const deletePromises = availabilities.map(async (availability) => {
|
let avs = availabilities.filter(av => av.type !== "assignment");
|
||||||
|
const deletePromises = avs.map(async (availability) => {
|
||||||
if (availability.id) {
|
if (availability.id) {
|
||||||
// console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id);
|
// console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id);
|
||||||
await axiosInstance.delete(urls.apiUrl + availability.id);
|
await axiosInstance.delete(urls.apiUrl + availability.id);
|
||||||
@ -233,9 +308,10 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
handleCompletion({ deleted: true });
|
handleCompletion({ deleted: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас");
|
//alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас");
|
||||||
console.log(JSON.stringify(error));
|
console.log(JSON.stringify(error));
|
||||||
toast.error(error.response?.data?.message || "An error occurred");
|
toast.error(error.response?.data?.message || "An error occurred");
|
||||||
|
fetchItemFromDB();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -294,8 +370,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
|
|
||||||
|
|
||||||
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
||||||
const [allDay, setAllDay] = useState(false);
|
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
||||||
|
|
||||||
const handleAllDayChange = (e) => {
|
const handleAllDayChange = (e) => {
|
||||||
const updatedSlots = slots.map(slot => ({
|
const updatedSlots = slots.map(slot => ({
|
||||||
...slot,
|
...slot,
|
||||||
@ -303,7 +378,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
}));
|
}));
|
||||||
setSlots(updatedSlots);
|
setSlots(updatedSlots);
|
||||||
setAllDay(e.target.checked)
|
setAllDay(e.target.checked)
|
||||||
setCanUpdate(true);
|
// setCanUpdate(slots.some(slot => slot.isChecked));
|
||||||
|
const anyChecked = updatedSlots.some(slot => slot.isChecked);
|
||||||
|
setCanUpdate(anyChecked);
|
||||||
console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots));
|
console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots));
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -358,9 +435,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="mb-1 flex justify-between items-center">
|
<div key={index} className="mb-1 flex justify-between items-center">
|
||||||
<label className={`checkbox-container flex items-center mb-2 ${allDay ? 'opacity-50' : ''}`}>
|
<label className={`checkbox-container flex items-center mb-2 `}>
|
||||||
<input type="checkbox" checked={slot.isChecked || allDay} onChange={() => handleSlotCheckedChange(slot)}
|
<input type="checkbox" checked={slot.isChecked || allDay} onChange={() => handleSlotCheckedChange(slot)}
|
||||||
disabled={allDay}
|
|
||||||
className="form-checkbox h-5 w-5 text-gray-600 mx-2" />
|
className="form-checkbox h-5 w-5 text-gray-600 mx-2" />
|
||||||
{slotLabel}
|
{slotLabel}
|
||||||
<span className="checkmark"></span>
|
<span className="checkmark"></span>
|
||||||
@ -368,11 +445,11 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
|
|
||||||
{/* Conditionally render transport checkbox based on slot being first or last */}
|
{/* Conditionally render transport checkbox based on slot being first or last */}
|
||||||
{slot.transportNeeded && (
|
{slot.transportNeeded && (
|
||||||
<label className={`checkbox-container flex items-center ${(!slot.isChecked || allDay) ? 'opacity-50' : ''}`}>
|
<label className={`checkbox-container flex items-center ${(!slot.isChecked) ? 'opacity-50' : ''}`}>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
className="form-checkbox h-5 w-5 text-gray-600 mx-2"
|
className="form-checkbox h-5 w-5 text-gray-600 mx-2"
|
||||||
checked={slot.isWithTransport}
|
checked={slot.isWithTransport}
|
||||||
disabled={!slot.isChecked || allDay}
|
disabled={!slot.isChecked}
|
||||||
onChange={() => handleTransportChange(slot)} />
|
onChange={() => handleTransportChange(slot)} />
|
||||||
{slot.isFirst ? 'Вземане' : 'Връщане'}
|
{slot.isFirst ? 'Вземане' : 'Връщане'}
|
||||||
<span className="checkmark"></span>
|
<span className="checkmark"></span>
|
||||||
@ -401,12 +478,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} />
|
<DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="mb-1">
|
|
||||||
{/* Time slot checkboxes */}
|
|
||||||
<TimeSlotCheckboxes slots={timeSlots} setSlots={setTimeSlots} items={availabilities} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<label className="checkbox-container">
|
<label className="checkbox-container">
|
||||||
@ -451,6 +522,13 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
<DatePicker label="До" value={repeatUntil} onChange={(value) => setRepeatUntil({ value })} />
|
<DatePicker label="До" value={repeatUntil} onChange={(value) => setRepeatUntil({ value })} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="mb-1">
|
||||||
|
{/* Time slot checkboxes */}
|
||||||
|
<TimeSlotCheckboxes slots={timeSlots} setSlots={setTimeSlots} items={availabilities} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
|
|
||||||
<div className="flex justify-between items-center flex-nowrap w-full p-1">
|
<div className="flex justify-between items-center flex-nowrap w-full p-1">
|
||||||
|
@ -36,7 +36,7 @@ model Availability {
|
|||||||
weekOfMonth Int?
|
weekOfMonth Int?
|
||||||
startTime DateTime
|
startTime DateTime
|
||||||
endTime DateTime
|
endTime DateTime
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
type AvailabilityType @default(Weekly)
|
type AvailabilityType @default(Weekly)
|
||||||
isWithTransport Boolean @default(false)
|
isWithTransport Boolean @default(false)
|
||||||
isFromPreviousAssignment Boolean @default(false)
|
isFromPreviousAssignment Boolean @default(false)
|
||||||
@ -69,7 +69,7 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
|
|||||||
dayOfMonth: null,
|
dayOfMonth: null,
|
||||||
startTime: "08:00",
|
startTime: "08:00",
|
||||||
endTime: "20:00",
|
endTime: "20:00",
|
||||||
isactive: true,
|
isActive: true,
|
||||||
repeatWeekly: false,
|
repeatWeekly: false,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
@ -165,21 +165,21 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
if (!availability.name) {
|
if (!availability.name) {
|
||||||
// availability.name = "От календара";
|
// availability.name = "От календара";
|
||||||
availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
|
availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
availability.dayofweek = common.getDayOfWeekNameEnEnum(availability.startTime);
|
availability.dayofweek = common.getDayOfWeekNameEnEnumForDate(availability.startTime);
|
||||||
if (availability.repeatWeekly) {
|
if (availability.repeatWeekly) {
|
||||||
availability.dayOfMonth = null;
|
availability.dayOfMonth = null;
|
||||||
|
availability.weekOfMonth = 0; //weekly recurrance - no need for week of month. special value 0
|
||||||
} else {
|
} else {
|
||||||
availability.endDate = null;
|
availability.endDate = null;
|
||||||
availability.dayOfMonth = availability.startTime.getDate();
|
availability.dayOfMonth = availability.startTime.getDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete availability.date; //remove date from availability as it is not part of the db model
|
delete availability.date; //remove date from availability as it is not part of the db model. we store the info in startDate and endDate
|
||||||
// ---------------------- CB UI --------------
|
// ---------------------- CB UI --------------
|
||||||
if (config.checkboxUI.enabled) {
|
if (config.checkboxUI.enabled) {
|
||||||
const selectedSlots = timeSlots.filter(slot => slot.isChecked);
|
const selectedSlots = timeSlots.filter(slot => slot.isChecked);
|
||||||
@ -554,11 +554,11 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
|
|||||||
|
|
||||||
<div className="mb-2 hidden">
|
<div className="mb-2 hidden">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input className="checkbox form-input" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={availability.isactive} autoComplete="off" />
|
<input className="checkbox form-input" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={availability.isActive} autoComplete="off" />
|
||||||
<label className="label" htmlFor="isactive">активно</label>
|
<label className="label" htmlFor="isActive">активно</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <input type="hidden" name="isactive" value={availability.isactive} /> */}
|
{/* <input type="hidden" name="isActive" value={availability.isActive} /> */}
|
||||||
|
|
||||||
<div className="panel-actions">
|
<div className="panel-actions">
|
||||||
<button className="action-button" onClick={() => handleCompletion()}> Отмени </button>
|
<button className="action-button" onClick={() => handleCompletion()}> Отмени </button>
|
||||||
|
@ -56,7 +56,7 @@ export default function AvailabilityList({ publisher, showNew }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items?.sort((a, b) => new Date(a.startTime) - new Date(b.startTime)).map(item => (
|
{items?.sort((a, b) => new Date(a.startTime) - new Date(b.startTime)).map(item => (
|
||||||
<tr key={item.id} availability={item} disabled={!item.isactive} className={`${item.isFromPreviousMonth ? 'bg-yellow-200' : ''} ${!item.isactive ? 'opacity-50' : ''}`}>
|
<tr key={item.id} availability={item} disabled={!item.isActive} className={`${item.isFromPreviousMonth ? 'bg-yellow-200' : ''} ${!item.isActive ? 'opacity-50' : ''}`}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
{item.dayOfMonth ? `${common.getDateFormated(new Date(item.startTime))}` : `Всеки(Всяка) ${common.getDayOfWeekName(new Date(item.startTime))}`}
|
{item.dayOfMonth ? `${common.getDateFormated(new Date(item.startTime))}` : `Всеки(Всяка) ${common.getDayOfWeekName(new Date(item.startTime))}`}
|
||||||
{/* {common.getDateFormated(new Date(item.startTime))} */}
|
{/* {common.getDateFormated(new Date(item.startTime))} */}
|
||||||
|
@ -1,49 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import axiosInstance from '../../src/axiosSecure';
|
import axiosInstance from '../../src/axiosSecure';
|
||||||
import PublisherSearchBox from '../publisher/PublisherSearchBox'; // Update the path
|
import PublisherSearchBox from '../publisher/PublisherSearchBox'; // Update the path
|
||||||
|
import Modal from '../Modal';
|
||||||
|
|
||||||
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
|
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
|
|
||||||
|
|
||||||
interface ModalProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
forDate: Date;
|
|
||||||
useFilterDate: boolean;
|
|
||||||
onUseFilterDateChange: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Modal({ children, isOpen, onClose, forDate, useFilterDate, onUseFilterDateChange }: ModalProps) {
|
|
||||||
if (!isOpen) return null;
|
|
||||||
const isValidDate = forDate instanceof Date && !isNaN(forDate.getTime());
|
|
||||||
console.log("forDate", forDate, isValidDate);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white p-4 rounded-md shadow-lg modal-content">
|
|
||||||
{isValidDate && (
|
|
||||||
<h2 className="text-xl font-bold mb-2">
|
|
||||||
<label className="cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={useFilterDate}
|
|
||||||
onChange={(e) => onUseFilterDateChange(e.target.checked)}
|
|
||||||
/>
|
|
||||||
{` на разположение ${common.getDateFormated(forDate)} или ${common.getDayOfWeekName(forDate)}`}
|
|
||||||
</label>
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
<button type="button" onClick={onClose} className="mt-4 text-red-500">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="fixed inset-0 bg-black opacity-50 modal-overlay" onClick={onClose}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
||||||
|
|
||||||
@ -52,10 +16,11 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
const [useFilterDate, setUseFilterDate] = useState(true);
|
const [useFilterDate, setUseFilterDate] = useState(true);
|
||||||
const [selectedPublisher, setSelectedPublisher] = useState(null);
|
const [selectedPublisher, setSelectedPublisher] = useState(null);
|
||||||
const [showCopyHint, setShowCopyHint] = useState(false);
|
const [showCopyHint, setShowCopyHint] = useState(false);
|
||||||
|
const [transportProvided, setTransportProvided] = useState(false);
|
||||||
// Update assignments when shift changes
|
// Update assignments when shift changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAssignments(shift.assignments);
|
setAssignments(shift.assignments);
|
||||||
|
setTransportProvided(!shift.requiresTransport || shift.assignments.some(ass => ass.isWithTransport));
|
||||||
}, [shift.assignments]);
|
}, [shift.assignments]);
|
||||||
|
|
||||||
const handleShiftClick = (shiftId) => {
|
const handleShiftClick = (shiftId) => {
|
||||||
@ -105,8 +70,8 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
const newAssignment = {
|
const newAssignment = {
|
||||||
publisher: { connect: { id: publisher.id } },
|
publisher: { connect: { id: publisher.id } },
|
||||||
shift: { connect: { id: shiftId } },
|
shift: { connect: { id: shiftId } },
|
||||||
//isactive: true,
|
//isActive: true,
|
||||||
isConfirmed: true
|
isConfirmed: true,
|
||||||
};
|
};
|
||||||
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
||||||
// Update the 'publisher' property of the returned data with the full publisher object
|
// Update the 'publisher' property of the returned data with the full publisher object
|
||||||
@ -124,9 +89,16 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
setTimeout(() => setShowCopyHint(false), 1500);
|
setTimeout(() => setShowCopyHint(false), 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function toggleTransport(assignment): Promise<void> {
|
||||||
|
try {
|
||||||
|
assignment.isWithTransport = !assignment.isWithTransport;
|
||||||
|
const { data } = await axiosInstance.put("/api/data/assignments/" + assignment.id,
|
||||||
|
{ isWithTransport: assignment.isWithTransport })
|
||||||
|
.then(() => {
|
||||||
|
setTransportProvided(assignments.some(ass => ass.isWithTransport))
|
||||||
|
});
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
||||||
@ -135,6 +107,7 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
||||||
<span className="text-lg font-semibold">
|
<span className="text-lg font-semibold">
|
||||||
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
||||||
|
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Copy All Names Button */}
|
{/* Copy All Names Button */}
|
||||||
@ -149,13 +122,13 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Assignments */}
|
{/* Assignments */}
|
||||||
{assignments.map((ass, index) => {
|
{assignments.map((ass, index) => {
|
||||||
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
||||||
|
|
||||||
// Determine border styles
|
// Determine border styles
|
||||||
let borderStyles = '';
|
let borderStyles = '';
|
||||||
|
let canTransport = false;
|
||||||
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
||||||
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers
|
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers
|
||||||
}
|
}
|
||||||
@ -172,10 +145,12 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
else {
|
else {
|
||||||
|
|
||||||
// checkig if the publisher is available for this assignment
|
// checkig if the publisher is available for this assignment
|
||||||
if (publisherInfo.availabilities?.some(av =>
|
const av = publisherInfo.availabilities?.find(av =>
|
||||||
av.startTime <= shift.startTime &&
|
av.startTime <= shift.startTime && av.endTime >= shift.endTime
|
||||||
av.endTime >= shift.endTime)) {
|
);
|
||||||
|
if (av) {
|
||||||
borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions
|
borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions
|
||||||
|
ass.canTransport = av.isWithTransportIn || av.isWithTransportOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publisherInfo.hasUpToDateAvailabilities) {
|
if (publisherInfo.hasUpToDateAvailabilities) {
|
||||||
@ -194,17 +169,29 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index}
|
<div key={index}
|
||||||
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-yellow-100' : 'bg-gray-100'} ${borderStyles}`}
|
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
|
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
|
||||||
<span className="text-gray-700">{publisherInfo.firstName} {publisherInfo.lastName}</span>
|
<span className="text-gray-700">{publisherInfo.firstName} {publisherInfo.lastName}</span>
|
||||||
<button onClick={() => removeAssignment(ass.id)}
|
<div className="flex items-left" >
|
||||||
className="text-white bg-red-500 hover:bg-red-600 px-3 py-1 ml-2 rounded-md"
|
{/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */}
|
||||||
>
|
{shift.requiresTransport && (
|
||||||
махни
|
<span
|
||||||
</button>
|
onClick={ass.canTransport ? () => toggleTransport(ass) : undefined}
|
||||||
|
className={`material-icons ${ass.isWithTransport ? 'text-green-500 font-bold' : (transportProvided ? 'text-gray-400 ' : 'text-orange-400 font-bold')} ${ass.canTransport ? ' cursor-pointer' : 'cursor-not-allowed'} px-3 py-1 ml-2 rounded-md`}
|
||||||
|
>
|
||||||
|
{ass.isWithTransport ? "транспорт" : ass.canTransport ? "може транспорт" : "без транспорт"} <LocalShippingIcon />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button onClick={() => removeAssignment(ass.id)} className="text-white bg-red-500 hover:bg-red-600 px-3 py-1 ml-2 rounded-md" >
|
||||||
|
махни
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -243,7 +230,7 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
showList={false}
|
showList={false}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,14 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
|
|
||||||
// Update internal state when `events` prop changes
|
// Update internal state when `events` prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatedEvents = events.map(event => ({
|
//if we have isBySystem - set type to assignment
|
||||||
|
let updatedEvents = events?.map(event => {
|
||||||
|
if (event.isBySystem) {
|
||||||
|
event.type = "assignment";
|
||||||
|
}
|
||||||
|
return 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),
|
||||||
@ -206,7 +213,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
// startTime: start,
|
// startTime: start,
|
||||||
// endTime: end,
|
// endTime: end,
|
||||||
// dayOfMonth: start.getDate(),
|
// dayOfMonth: start.getDate(),
|
||||||
// isactive: true,
|
// isActive: true,
|
||||||
// publisherId: publisherId,
|
// publisherId: publisherId,
|
||||||
// // Add any other initial values needed
|
// // Add any other initial values needed
|
||||||
// //set dayOfMonth to null, so that we repeat the availability every week
|
// //set dayOfMonth to null, so that we repeat the availability every week
|
||||||
@ -273,9 +280,14 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
//if event.type is availability show in blue. if it is schedule - green if confirmed, yellow if not confirmed
|
//if event.type is availability show in blue. if it is schedule - green if confirmed, yellow if not confirmed
|
||||||
//if event is not active - show in gray
|
//if event is not active - show in gray
|
||||||
let bgColorClass = 'bg-gray-500'; // Default color for inactive events
|
let bgColorClass = 'bg-gray-500'; // Default color for inactive events
|
||||||
var bgColor = event.isactive ? "" : "bg-gray-500";
|
var bgColor = event.isActive ? "" : "bg-gray-500";
|
||||||
|
//ToDo: fix this. maybe we're missing some properties
|
||||||
|
// if (event.isFromPreviousMonth) {
|
||||||
|
// // set opacity to 0.5
|
||||||
|
// bgColor = "bg-orange-500";
|
||||||
|
// }
|
||||||
if (event.type === "assignment") {
|
if (event.type === "assignment") {
|
||||||
bgColor = event.isTentative ? "bg-red-500" : (event.isConfirmed ? "bg-green-500" : "bg-yellow-500");
|
bgColor = event.isBySystem ? "bg-red-500" : (event.isConfirmed ? "bg-green-500" : "bg-yellow-500");
|
||||||
|
|
||||||
//event.title = event.publisher.name; //ToDo: add other publishers names
|
//event.title = event.publisher.name; //ToDo: add other publishers names
|
||||||
//event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime);
|
//event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime);
|
||||||
@ -297,11 +309,14 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eventStyle = {
|
eventStyle = {
|
||||||
...style,
|
...style,
|
||||||
// backgroundColor: bgColorClass,
|
// backgroundColor: bgColorClass,
|
||||||
//height: "50px",
|
//height: "50px",
|
||||||
//color: 'white',
|
//color: 'white',
|
||||||
|
//if (event.isFromPreviousAssignment) { set opacity to 0.5 }
|
||||||
|
// opacity: event.isFromPreviousMonth ? 0.5 : 1,
|
||||||
whiteSpace: 'normal', // Allow the text to wrap to the next line
|
whiteSpace: 'normal', // Allow the text to wrap to the next line
|
||||||
overflow: 'hidden', // Hide overflowed content
|
overflow: 'hidden', // Hide overflowed content
|
||||||
textOverflow: 'ellipsis' // Add ellipsis to text that's too long to fit
|
textOverflow: 'ellipsis' // Add ellipsis to text that's too long to fit
|
||||||
@ -319,7 +334,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
toast.info("Потвърдено!", { autoClose: 2000 });
|
toast.info("Потвърдено!", { autoClose: 2000 });
|
||||||
// Update the event data
|
// Update the event data
|
||||||
event.isConfirmed = true;
|
event.isConfirmed = true;
|
||||||
event.isTentative = false;
|
event.isBySystem = false;
|
||||||
// Update the events array by first removing the old event and then adding the updated one
|
// Update the events array by first removing the old event and then adding the updated one
|
||||||
setEvents(currentEvents => {
|
setEvents(currentEvents => {
|
||||||
const filteredEvents = currentEvents.filter(e => e.id !== event.id);
|
const filteredEvents = currentEvents.filter(e => e.id !== event.id);
|
||||||
@ -328,7 +343,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
//store the updated event in the database
|
//store the updated event in the database
|
||||||
var assignment = {
|
var assignment = {
|
||||||
isConfirmed: true,
|
isConfirmed: true,
|
||||||
isTentative: false
|
isBySystem: false
|
||||||
};
|
};
|
||||||
axiosInstance.put('/api/data/assignments/' + event.id, assignment)
|
axiosInstance.put('/api/data/assignments/' + event.id, assignment)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -356,7 +371,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
</span> */}
|
</span> */}
|
||||||
|
|
||||||
{/* Confirm Icon */}
|
{/* Confirm Icon */}
|
||||||
{!event.isConfirmed && (
|
{/* {!event.isConfirmed && (
|
||||||
<span
|
<span
|
||||||
className=" cursor-pointer rounded-full bg-green-500 text-white flex items-center justify-center"
|
className=" cursor-pointer rounded-full bg-green-500 text-white flex items-center justify-center"
|
||||||
style={{ width: '24px', height: '24px' }} // Adjust the size as needed
|
style={{ width: '24px', height: '24px' }} // Adjust the size as needed
|
||||||
@ -364,7 +379,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
</span>
|
</span>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -388,7 +403,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
// orange-500 from Tailwind CSS
|
// orange-500 from Tailwind CSS
|
||||||
backgroundColor = '#f56565';
|
backgroundColor = '#f56565';
|
||||||
}
|
}
|
||||||
if (event.isactive) {
|
if (event.isActive) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'assignment':
|
case 'assignment':
|
||||||
backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS
|
backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS
|
||||||
|
@ -15,7 +15,7 @@ model CartEvent {
|
|||||||
shiftDuration Int
|
shiftDuration Int
|
||||||
shifts Shift[]
|
shifts Shift[]
|
||||||
dayofweek DayOfWeek
|
dayofweek DayOfWeek
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}*/
|
}*/
|
||||||
interface Location {
|
interface Location {
|
||||||
id: number;
|
id: number;
|
||||||
@ -174,8 +174,8 @@ export default function CartEventForm(props: IProps) {
|
|||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input className="checkbox" type="checkbox" name="isactive" id="isactive" checked={evt.isactive} onChange={handleChange} />
|
<input className="checkbox" type="checkbox" name="isActive" id="isActive" checked={evt.isActive} onChange={handleChange} />
|
||||||
<label className='label align-text-bottom' htmlFor="isactive">Active</label>
|
<label className='label align-text-bottom' htmlFor="isActive">Active</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-actions">
|
<div className="panel-actions">
|
||||||
|
@ -28,11 +28,11 @@ export default function LocationCard({ location }) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
id={`location-card-${location.id}`}
|
id={`location-card-${location.id}`}
|
||||||
className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3 cursor-pointer ${location.isactive ? 'text-gray-900 dark:text-white font-bold' : 'text-gray-400 dark:text-gray-600'}`}
|
className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3 cursor-pointer ${location.isActive ? 'text-gray-900 dark:text-white font-bold' : 'text-gray-400 dark:text-gray-600'}`}
|
||||||
onClick={() => router.push(`/cart/locations/edit/${location.id}`)}
|
onClick={() => router.push(`/cart/locations/edit/${location.id}`)}
|
||||||
>
|
>
|
||||||
<h5 className={`mb-2 text-2xl tracking-tight`}>
|
<h5 className={`mb-2 text-2xl tracking-tight`}>
|
||||||
{location.name} ({location.isactive ? "active" : "inactive"})
|
{location.name} ({location.isActive ? "active" : "inactive"})
|
||||||
</h5>
|
</h5>
|
||||||
<p className="font-normal text-gray-700 dark:text-gray-200">
|
<p className="font-normal text-gray-700 dark:text-gray-200">
|
||||||
{location.address}
|
{location.address}
|
||||||
|
@ -19,7 +19,7 @@ const common = require('src/helpers/common');
|
|||||||
// id Int @id @default(autoincrement())
|
// id Int @id @default(autoincrement())
|
||||||
// name String
|
// name String
|
||||||
// address String
|
// address String
|
||||||
// isactive Boolean @default(true)
|
// isActive Boolean @default(true)
|
||||||
// content String? @db.Text
|
// content String? @db.Text
|
||||||
// cartEvents CartEvent[]
|
// cartEvents CartEvent[]
|
||||||
// reports Report[]
|
// reports Report[]
|
||||||
@ -65,7 +65,7 @@ export default function LocationForm() {
|
|||||||
const [location, set] = useState({
|
const [location, set] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
address: "",
|
address: "",
|
||||||
isactive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// const [isEdit, setIsEdit] = useState(false);
|
// const [isEdit, setIsEdit] = useState(false);
|
||||||
@ -171,11 +171,11 @@ export default function LocationForm() {
|
|||||||
<input className="textbox"
|
<input className="textbox"
|
||||||
placeholder="address" id="address" name="address" onChange={handleChange} value={location.address} autoComplete="off" />
|
placeholder="address" id="address" name="address" onChange={handleChange} value={location.address} autoComplete="off" />
|
||||||
</div>
|
</div>
|
||||||
{/* UI for Location.isactive */}
|
{/* UI for Location.isActive */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input className="checkbox form-input" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={location.isactive} autoComplete="off" />
|
<input className="checkbox form-input" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={location.isActive} autoComplete="off" />
|
||||||
<label className="label" htmlFor="isactive">Активна</label>
|
<label className="label" htmlFor="isActive">Активна</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* backupLocation */}
|
{/* backupLocation */}
|
||||||
|
@ -57,7 +57,7 @@ export default function PublisherCard({ publisher }) {
|
|||||||
return isCardVisible ? (
|
return isCardVisible ? (
|
||||||
// className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
|
// className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
|
||||||
<div id={`publisher-card-${publisher.id}`} className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3
|
<div id={`publisher-card-${publisher.id}`} className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3
|
||||||
${!publisher.isactive ? "opacity-50 bg-gray-200 border-gray-300 text-gray-400" : (publisher.isImported ? "bg-orange-50" : (publisher.isTrained ? "bg-white" : "bg-red-50"))}`}
|
${!publisher.isActive ? "opacity-50 bg-gray-200 border-gray-300 text-gray-400" : (publisher.isImported ? "bg-orange-50" : (publisher.isTrained ? "bg-white" : "bg-red-50"))}`}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={`/cart/publishers/edit/${publisher.id}`}
|
href={`/cart/publishers/edit/${publisher.id}`}
|
||||||
@ -66,7 +66,7 @@ export default function PublisherCard({ publisher }) {
|
|||||||
>
|
>
|
||||||
|
|
||||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
{publisher.firstName} {publisher.lastName} ({publisher.isactive ? "active" : "inactive"})
|
{publisher.firstName} {publisher.lastName} ({publisher.isActive ? "active" : "inactive"})
|
||||||
</h5>
|
</h5>
|
||||||
<div className="font-normal text-gray-700 dark:text-gray-200">
|
<div className="font-normal text-gray-700 dark:text-gray-200">
|
||||||
<p> {publisher.assignments.length} смени общо</p>
|
<p> {publisher.assignments.length} смени общо</p>
|
||||||
|
@ -23,7 +23,7 @@ import { UserRole } from "@prisma/client";
|
|||||||
// lastName String
|
// lastName String
|
||||||
// email String @unique
|
// email String @unique
|
||||||
// phone String?
|
// phone String?
|
||||||
// isactive Boolean @default(true)
|
// isActive Boolean @default(true)
|
||||||
// isImported Boolean @default(false)
|
// isImported Boolean @default(false)
|
||||||
// age Int?
|
// age Int?
|
||||||
// availabilities Availability[]
|
// availabilities Availability[]
|
||||||
@ -75,7 +75,7 @@ export default function PublisherForm({ item, me }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [publisher, set] = useState(item || {
|
const [publisher, set] = useState(item || {
|
||||||
isactive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = ({ target }) => {
|
const handleChange = ({ target }) => {
|
||||||
@ -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 >
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -4,8 +4,7 @@ import toast from "react-hot-toast";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const PublisherInlineForm = ({ publisherId, initialShiftsPerMonth }) => {
|
const PublisherInlineForm = ({ publisherId, initialShiftsPerMonth }) => {
|
||||||
const [desiredShiftsPerMonth, setDesiredShiftsPerMonth] = useState(initialShiftsPerMonth);
|
const [desiredShiftsPerMonth, setDesiredShiftsPerMonth] = useState(initialShiftsPerMonth || 2);
|
||||||
const router = useRouter();
|
|
||||||
const storedValue = useRef(initialShiftsPerMonth);
|
const storedValue = useRef(initialShiftsPerMonth);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -18,7 +18,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS
|
|||||||
const fetchPublishers = async () => {
|
const fetchPublishers = async () => {
|
||||||
console.log("fetchPublishers called");
|
console.log("fetchPublishers called");
|
||||||
try {
|
try {
|
||||||
let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isactive&searchText=${searchText}&availabilities=false`;
|
let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isActive&searchText=${searchText}&availabilities=false`;
|
||||||
|
|
||||||
if (filterDate) {
|
if (filterDate) {
|
||||||
url += `&filterDate=${common.getISODateOnly(filterDate)}`;
|
url += `&filterDate=${common.getISODateOnly(filterDate)}`;
|
||||||
@ -29,7 +29,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS
|
|||||||
|
|
||||||
const { data: publishersData } = await axiosInstance.get(url);
|
const { data: publishersData } = await axiosInstance.get(url);
|
||||||
//setPublishers(publishersData);
|
//setPublishers(publishersData);
|
||||||
const activePublishers = publishersData.filter(publisher => publisher.isactive === true);
|
const activePublishers = publishersData.filter(publisher => publisher.isActive === true);
|
||||||
setPublishers(activePublishers);
|
setPublishers(activePublishers);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -35,7 +35,7 @@ const ShiftsList = ({ assignments, selectedtab }: ShiftsListProps) => {
|
|||||||
try {
|
try {
|
||||||
var assignment = (await axiosInstance.get("/api/data/assignments/" + id)).data;
|
var assignment = (await axiosInstance.get("/api/data/assignments/" + id)).data;
|
||||||
assignment.isConfirmed = false;
|
assignment.isConfirmed = false;
|
||||||
assignment.isTentative = true;
|
// ! assignment.isTentative = true;
|
||||||
// assignment.isDeleted = true;
|
// assignment.isDeleted = true;
|
||||||
await axiosInstance.put("/api/data/assignments/" + id, assignment);
|
await axiosInstance.put("/api/data/assignments/" + id, assignment);
|
||||||
toast.success("Shift Tentative", {
|
toast.success("Shift Tentative", {
|
||||||
|
@ -102,7 +102,7 @@ export default function ExperienceForm({ publisherId, assgnmentId, existingItem,
|
|||||||
try {
|
try {
|
||||||
const response = await axiosInstance.post('/api/data/reports', item);
|
const response = await axiosInstance.post('/api/data/reports', item);
|
||||||
console.log(response);
|
console.log(response);
|
||||||
toast.success("Случката е записана. Благодарим Ви!");
|
toast.success("Случката е записана. Благодаря!");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (onDone) {
|
if (onDone) {
|
||||||
onDone();
|
onDone();
|
||||||
|
@ -78,7 +78,7 @@ export default function FeedbackForm({ publisherId, onDone }) {
|
|||||||
try {
|
try {
|
||||||
const response = await axiosInstance.post('/api/data/reports', item);
|
const response = await axiosInstance.post('/api/data/reports', item);
|
||||||
console.log(response);
|
console.log(response);
|
||||||
toast.success("Благодарим Ви за вашия отзив!");
|
toast.success("Благодаря за отзива!");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (onDone) {
|
if (onDone) {
|
||||||
onDone();
|
onDone();
|
||||||
|
@ -101,7 +101,7 @@ export default function ReportForm({ shiftId, existingItem, onDone }) {
|
|||||||
try {
|
try {
|
||||||
const response = await axiosInstance.post('/api/data/reports', item);
|
const response = await axiosInstance.post('/api/data/reports', item);
|
||||||
console.log(response);
|
console.log(response);
|
||||||
toast.success("Гоово. Благодарим Ви за отчета!");
|
toast.success("Гоово. Благодаря за отчета!");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (onDone) {
|
if (onDone) {
|
||||||
onDone();
|
onDone();
|
||||||
|
@ -99,7 +99,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
|
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
|
||||||
const locationsData = response.data
|
const locationsData = response.data
|
||||||
.filter(location => location.isactive === true)
|
.filter(location => location.isActive === true)
|
||||||
.map(location => ({
|
.map(location => ({
|
||||||
text: location.name,
|
text: location.name,
|
||||||
url: `/cart/locations/${location.id}`,
|
url: `/cart/locations/${location.id}`,
|
||||||
|
@ -12,7 +12,7 @@ module.exports = {
|
|||||||
pageExtensions: ['ts', 'tsx', 'md', 'mdx'], // Replace `jsx?` with `tsx?`
|
pageExtensions: ['ts', 'tsx', 'md', 'mdx'], // Replace `jsx?` with `tsx?`
|
||||||
env: {
|
env: {
|
||||||
env: process.env.NODE_ENV,
|
env: process.env.NODE_ENV,
|
||||||
server: 'http://' + process.env.NEXT_PUBLIC_HOST + ':' + process.env.NEXT_PUBLIC_PORT + '',
|
server: process.env.NEXT_PUBLIC_PUBLIC_URL
|
||||||
},
|
},
|
||||||
webpack(config, { isServer }) {
|
webpack(config, { isServer }) {
|
||||||
|
|
||||||
|
69
package-lock.json
generated
69
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "0.9.9",
|
"version": "1.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "0.9.9",
|
"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",
|
||||||
@ -16,7 +16,7 @@
|
|||||||
"@mui/material": "^5.15.10",
|
"@mui/material": "^5.15.10",
|
||||||
"@mui/x-date-pickers": "^6.19.4",
|
"@mui/x-date-pickers": "^6.19.4",
|
||||||
"@premieroctet/next-crud": "^3.0.0",
|
"@premieroctet/next-crud": "^3.0.0",
|
||||||
"@prisma/client": "^5.10.2",
|
"@prisma/client": "^5.11.0",
|
||||||
"@react-pdf/renderer": "^3.3.8",
|
"@react-pdf/renderer": "^3.3.8",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@types/multer": "^1.4.11",
|
"@types/multer": "^1.4.11",
|
||||||
@ -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",
|
||||||
@ -90,7 +91,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"prisma": "^5.10.2"
|
"prisma": "^5.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@ -2282,9 +2283,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "5.10.2",
|
"version": "5.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.11.0.tgz",
|
||||||
"integrity": "sha512-ef49hzB2yJZCvM5gFHMxSFL9KYrIP9udpT5rYo0CsHD4P9IKj473MbhU1gjKKftiwWBTIyrt9jukprzZXazyag==",
|
"integrity": "sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.13"
|
"node": ">=16.13"
|
||||||
@ -2299,39 +2300,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "5.10.2",
|
"version": "5.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz",
|
||||||
"integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==",
|
"integrity": "sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "5.10.2",
|
"version": "5.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.11.0.tgz",
|
||||||
"integrity": "sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw==",
|
"integrity": "sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.10.2",
|
"@prisma/debug": "5.11.0",
|
||||||
"@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9",
|
"@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102",
|
||||||
"@prisma/fetch-engine": "5.10.2",
|
"@prisma/fetch-engine": "5.11.0",
|
||||||
"@prisma/get-platform": "5.10.2"
|
"@prisma/get-platform": "5.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9",
|
"version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz",
|
||||||
"integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==",
|
"integrity": "sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "5.10.2",
|
"version": "5.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz",
|
||||||
"integrity": "sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg==",
|
"integrity": "sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.10.2",
|
"@prisma/debug": "5.11.0",
|
||||||
"@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9",
|
"@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102",
|
||||||
"@prisma/get-platform": "5.10.2"
|
"@prisma/get-platform": "5.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/generator-helper": {
|
"node_modules/@prisma/generator-helper": {
|
||||||
@ -2348,12 +2349,12 @@
|
|||||||
"integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA=="
|
"integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA=="
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "5.10.2",
|
"version": "5.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.11.0.tgz",
|
||||||
"integrity": "sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg==",
|
"integrity": "sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "5.10.2"
|
"@prisma/debug": "5.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/internals": {
|
"node_modules/@prisma/internals": {
|
||||||
@ -9294,13 +9295,13 @@
|
|||||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "5.10.2",
|
"version": "5.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.11.0.tgz",
|
||||||
"integrity": "sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ==",
|
"integrity": "sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "5.10.2"
|
"@prisma/engines": "5.11.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
|
19
package.json
19
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "0.9.9",
|
"version": "1.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "JW PW Web App",
|
"description": "JW PW Web App",
|
||||||
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
||||||
@ -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": "cross-env NODE_ENV=development 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": {
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"@mui/material": "^5.15.10",
|
"@mui/material": "^5.15.10",
|
||||||
"@mui/x-date-pickers": "^6.19.4",
|
"@mui/x-date-pickers": "^6.19.4",
|
||||||
"@premieroctet/next-crud": "^3.0.0",
|
"@premieroctet/next-crud": "^3.0.0",
|
||||||
"@prisma/client": "^5.10.2",
|
"@prisma/client": "^5.11.0",
|
||||||
"@react-pdf/renderer": "^3.3.8",
|
"@react-pdf/renderer": "^3.3.8",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@types/multer": "^1.4.11",
|
"@types/multer": "^1.4.11",
|
||||||
@ -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",
|
||||||
@ -107,6 +108,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"prisma": "^5.10.2"
|
"prisma": "^5.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -35,7 +35,7 @@ export default function App({
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session} >
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
|
@ -24,12 +24,12 @@ import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshT
|
|||||||
export const authOptions: NextAuthOptions = {
|
export const authOptions: NextAuthOptions = {
|
||||||
// https://next-auth.js.org/configuration/providers/oauth
|
// https://next-auth.js.org/configuration/providers/oauth
|
||||||
|
|
||||||
site: process.env.NEXTAUTH_URL,
|
site: process.env.NEXT_PUBLIC_PUBLIC_URL,
|
||||||
secret: process.env.NEXTAUTH_SECRET, // Ensure you have this set in your .env file
|
secret: process.env.NEXTAUTH_SECRET, // Ensure you have this set in your .env file
|
||||||
//adapter: PrismaAdapter(prisma),
|
//adapter: PrismaAdapter(prisma),
|
||||||
providers: [
|
providers: [
|
||||||
// register new URL at https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
// register new URL at https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||||
//Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwhitnessing.com/
|
//Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwitnessingmwitnessing.com/
|
||||||
GoogleProvider({
|
GoogleProvider({
|
||||||
clientId: process.env.GOOGLE_ID,
|
clientId: process.env.GOOGLE_ID,
|
||||||
clientSecret: process.env.GOOGLE_SECRET,
|
clientSecret: process.env.GOOGLE_SECRET,
|
||||||
@ -41,11 +41,15 @@ export const authOptions: NextAuthOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
AzureADProvider({
|
AppleProvider({
|
||||||
clientId: process.env.AZURE_AD_CLIENT_ID,
|
clientId: process.env.APPLE_ID,
|
||||||
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
clientSecret: process.env.APPLE_SECRET
|
||||||
tenantId: process.env.AZURE_AD_TENANT_ID,
|
|
||||||
}),
|
}),
|
||||||
|
// AzureADProvider({
|
||||||
|
// clientId: process.env.AZURE_AD_CLIENT_ID,
|
||||||
|
// clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
||||||
|
// tenantId: process.env.AZURE_AD_TENANT_ID,
|
||||||
|
// }),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
// The name to display on the sign in form (e.g. 'Sign in with...')
|
// The name to display on the sign in form (e.g. 'Sign in with...')
|
||||||
name: 'Credentials',
|
name: 'Credentials',
|
||||||
@ -87,11 +91,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
// AppleProvider({
|
|
||||||
// clientId: process.env.APPLE_ID,
|
|
||||||
// clientSecret: process.env.APPLE_SECRET
|
|
||||||
// })
|
|
||||||
/*
|
/*
|
||||||
EmailProvider({
|
EmailProvider({
|
||||||
server: {
|
server: {
|
||||||
|
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));
|
||||||
|
|
@ -1,12 +1,13 @@
|
|||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { DayOfWeek } from '@prisma/client';
|
import { DayOfWeek, AvailabilityType } from '@prisma/client';
|
||||||
const common = require('../../src/helpers/common');
|
const common = require('../../src/helpers/common');
|
||||||
const data = require('../../src/helpers/data');
|
const data = require('../../src/helpers/data');
|
||||||
const subq = require('../../prisma/bl/subqueries');
|
const subq = require('../../prisma/bl/subqueries');
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { all } from "axios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -29,7 +30,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
var action = req.query.action;
|
var action = req.query.action;
|
||||||
var filter = req.query.filter;
|
var filter = req.query.filter;
|
||||||
let date: Date;
|
let date: Date, monthInfo: any;
|
||||||
if (req.query.date) {
|
if (req.query.date) {
|
||||||
date = new Date(req.query.date);
|
date = new Date(req.query.date);
|
||||||
//date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead)
|
//date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead)
|
||||||
@ -77,7 +78,7 @@ export default async function handler(req, res) {
|
|||||||
//gets publisher by names with availabilities and assignments
|
//gets publisher by names with availabilities and assignments
|
||||||
case "deleteAvailabilityForPublisher":
|
case "deleteAvailabilityForPublisher":
|
||||||
let publisherId = req.query.publisherId;
|
let publisherId = req.query.publisherId;
|
||||||
let dateFor, monthInfo;
|
let dateFor;
|
||||||
if (req.query.date) {
|
if (req.query.date) {
|
||||||
dateFor = new Date(req.query.date);
|
dateFor = new Date(req.query.date);
|
||||||
//get month info from date
|
//get month info from date
|
||||||
@ -143,7 +144,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
case "getUnassignedPublishers":
|
case "getUnassignedPublishers":
|
||||||
//let monthInfo = common.getMonthDatesInfo(date);
|
//let monthInfo = common.getMonthDatesInfo(date);
|
||||||
let allPubs = await filterPublishers("id,firstName,lastName,email,isactive".split(","), "", date, true, true, false);
|
let allPubs = await filterPublishers("id,firstName,lastName,email,isActive".split(","), "", date, true, true, false);
|
||||||
let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0);
|
let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0);
|
||||||
res.status(200).json(unassignedPubs);
|
res.status(200).json(unassignedPubs);
|
||||||
break;
|
break;
|
||||||
@ -209,7 +210,134 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
res.status(200).json(shiftsForDate);
|
res.status(200).json(shiftsForDate);
|
||||||
break;
|
break;
|
||||||
|
case "copyOldAvailabilities":
|
||||||
|
//get all publishers that don't have availabilities for the current month
|
||||||
|
monthInfo = common.getMonthDatesInfo(date);
|
||||||
|
// await prisma.availability.deleteMany({
|
||||||
|
// where: {
|
||||||
|
// startTime: {
|
||||||
|
// gte: monthInfo.firstMonday,
|
||||||
|
// },
|
||||||
|
// isFromPreviousMonth: true
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
let outdatedPubs = await prisma.publisher.findMany({
|
||||||
|
where: {
|
||||||
|
availabilities: {
|
||||||
|
none: {
|
||||||
|
startTime: {
|
||||||
|
gte: monthInfo.firstMonday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
availabilities: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
outdatedPubs.forEach(async pub => {
|
||||||
|
// avail.startTime >= monthInfo.firstMonday
|
||||||
|
//get prev month date:
|
||||||
|
let prevMonth = new Date(monthInfo.firstMonday);
|
||||||
|
prevMonth.setMonth(prevMonth.getMonth() - 1);
|
||||||
|
let prevMonthInfo = common.getMonthDatesInfo(prevMonth);
|
||||||
|
pub.availabilities = pub.availabilities.filter(avail => avail.startTime > prevMonthInfo.firstMonday);
|
||||||
|
console.log("" + pub.firstName + " " + pub.lastName + " copying " + pub.availabilities.length + " availabilities from previous months.");
|
||||||
|
pub.availabilities.forEach(async avail => {
|
||||||
|
//get the new date based on the day of week and week of month
|
||||||
|
if (!avail.weekOfMonth) {
|
||||||
|
avail.weekOfMonth = common.getWeekOfMonth(avail.startTime)
|
||||||
|
}
|
||||||
|
let origMonthInfo = common.getMonthDatesInfo(avail.startTime);
|
||||||
|
|
||||||
|
let newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, avail.weekOfMonth, avail.dayofweek, avail.startTime);
|
||||||
|
//ToDo: fix double check. also check if we're in 5th week and the month has 4 weeks
|
||||||
|
// const availability = await data.findPublisherAvailability(publisher.id, newStart);
|
||||||
|
// if (availability) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
let newEnd = new Date(newStart.getTime());
|
||||||
|
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
|
||||||
|
let data = {
|
||||||
|
publisherId: pub.id,
|
||||||
|
dayOfMonth: null,
|
||||||
|
dayofweek: avail.dayofweek || common.getDayOfWeekNameEnEnumForDate(avail.startTime),
|
||||||
|
weekOfMonth: avail.weekofMonth || common.getWeekOfMonth(avail.startTime),
|
||||||
|
// null for auto generated availabilities
|
||||||
|
//dateOfEntry: new Date(), //avail.dateOfEntry || avail.startTime,
|
||||||
|
startTime: newStart,
|
||||||
|
endTime: newEnd,
|
||||||
|
type: AvailabilityType.Monthly,
|
||||||
|
isFromPreviousMonth: true,
|
||||||
|
name: avail.name || "старо предпочитание",
|
||||||
|
// parentAvailabilityId: avail.id
|
||||||
|
parentAvailability: {
|
||||||
|
connect: {
|
||||||
|
id: avail.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.availability.create({ data: data });
|
||||||
|
|
||||||
|
//if month has 5 weeks and the monthInfo has 4 weeks copy the availabilities also from the 1st week to the 5th week
|
||||||
|
if (monthInfo.nrOfWeeks == 5 && avail.weekOfMonth == 1 && origMonthInfo.nrOfWeeks == 4) {
|
||||||
|
newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, 5, avail.dayofweek, avail.startTime);
|
||||||
|
newEnd = new Date(newStart.getTime());
|
||||||
|
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
|
||||||
|
data.weekOfMonth = 5;
|
||||||
|
data.startTime = newStart;
|
||||||
|
data.endTime = newEnd;
|
||||||
|
await prisma.availability.create({ data: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//convert old assignments to availabilities
|
||||||
|
|
||||||
|
res.status(200).json({ "message": "ok" });
|
||||||
|
break;
|
||||||
|
case "deleteCopiedAvailabilities":
|
||||||
|
//delete all availabilities that are copied from previous months
|
||||||
|
monthInfo = common.getMonthDatesInfo(date);
|
||||||
|
await prisma.availability.deleteMany({
|
||||||
|
where: {
|
||||||
|
startTime: {
|
||||||
|
gte: monthInfo.firstMonday,
|
||||||
|
},
|
||||||
|
isFromPreviousMonth: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case "replaceInAssignment":
|
||||||
|
const { oldPublisherId, newPublisherId, shiftId } = req.method === "POST" ? req.body : req.query;
|
||||||
|
|
||||||
|
const result = await replaceInAssignment(oldPublisherId, newPublisherId, shiftId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "updateShifts":
|
||||||
|
//get all shifts for the month and publish them (we pass date )
|
||||||
|
let monthInfo = common.getMonthDatesInfo(date);
|
||||||
|
let isPublished = common.parseBool(req.query.isPublished);
|
||||||
|
let updated = await prisma.shift.updateMany({
|
||||||
|
where: {
|
||||||
|
startTime: {
|
||||||
|
gte: new Date(monthInfo.firstMonday.getFullYear(), monthInfo.firstMonday.getMonth(), 1),
|
||||||
|
lt: new Date(monthInfo.lastSunday.getFullYear(), monthInfo.lastSunday.getMonth() + 1, 1),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isPublished: isPublished
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("Updated shifts: " + updated.count);
|
||||||
|
res.status(200).json({ "message": "ok" });
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
@ -224,6 +352,7 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getMonthlyStatistics(selectFields, filterDate) {
|
export async function getMonthlyStatistics(selectFields, filterDate) {
|
||||||
|
|
||||||
let publishers = [];
|
let publishers = [];
|
||||||
@ -406,7 +535,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
let dayOfWeekEnum: DayOfWeek
|
let dayOfWeekEnum: DayOfWeek
|
||||||
if (filterDate) {
|
if (filterDate) {
|
||||||
// Determine day of week using common function
|
// Determine day of week using common function
|
||||||
dayOfWeekEnum = common.getDayOfWeekNameEnEnum(filterDate);
|
dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate);
|
||||||
if (filterDate.getHours() > 21 || filterDate.getHours() < 6) {
|
if (filterDate.getHours() > 21 || filterDate.getHours() < 6) {
|
||||||
filterDate.setHours(0, 0, 0, 0); // Set to midnight
|
filterDate.setHours(0, 0, 0, 0); // Set to midnight
|
||||||
}
|
}
|
||||||
@ -435,8 +564,8 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
// also, permanent weekly availabilities will have dayOfMonth = null and type = 0
|
// also, permanent weekly availabilities will have dayOfMonth = null and type = 0
|
||||||
// for 0 we will match by dayOfWeekEnum and times
|
// for 0 we will match by dayOfWeekEnum and times
|
||||||
// for 1 we will match by exact date and times
|
// for 1 we will match by exact date and times
|
||||||
// for 2 we will match by dayofweek, weeknr and times
|
// for 2 we will match by dayofweek, weekOfMonth and times
|
||||||
// for 3 we will match by dayofweek, weeknr and times - this is the same as 2, but we will not count them as availabilities for the current month
|
// for 3 we will match by dayofweek, weekOfMonth and times - this is the same as 2, but we will not count them as availabilities for the current month
|
||||||
|
|
||||||
|
|
||||||
// generaion of schedule:
|
// generaion of schedule:
|
||||||
@ -451,6 +580,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
//substract the time difference between from ISO string and local time
|
//substract the time difference between from ISO string and local time
|
||||||
const offset = filterDate.getTimezoneOffset() * 60000; // offset in milliseconds
|
const offset = filterDate.getTimezoneOffset() * 60000; // offset in milliseconds
|
||||||
var dateAsISO = new Date(filterDate.getTime() + offset);
|
var dateAsISO = new Date(filterDate.getTime() + offset);
|
||||||
|
//if full day, match by date only
|
||||||
if (filterDate.getHours() == 0 || dateAsISO.getHours() == 0) {
|
if (filterDate.getHours() == 0 || dateAsISO.getHours() == 0) {
|
||||||
whereClause["availabilities"] = {
|
whereClause["availabilities"] = {
|
||||||
some: {
|
some: {
|
||||||
@ -464,16 +594,26 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
||||||
// This includes availabilities from previous assignments but not with preference
|
// This includes availabilities from previous assignments but not with preference
|
||||||
{
|
{
|
||||||
dayOfMonth: null,
|
dayOfMonth: null, // includes monthly and weekly repeats
|
||||||
dayofweek: dayOfWeekEnum,
|
dayofweek: dayOfWeekEnum,
|
||||||
// ToDo: and weekNr
|
// ToDo: and weekOfMonth
|
||||||
//startTime: { gte: currentMonthStart },
|
startTime: { lte: filterDate },
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
||||||
|
{ endDate: { gte: filterDate } },
|
||||||
|
{ endDate: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//if not full day, match by date and time
|
||||||
else {
|
else {
|
||||||
|
//match exact time (should be same as data.findPublisherAvailability())
|
||||||
whereClause["availabilities"] = {
|
whereClause["availabilities"] = {
|
||||||
some: {
|
some: {
|
||||||
OR: [
|
OR: [
|
||||||
@ -487,12 +627,14 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
{
|
{
|
||||||
dayOfMonth: null,
|
dayOfMonth: null,
|
||||||
dayofweek: dayOfWeekEnum,
|
dayofweek: dayOfWeekEnum,
|
||||||
|
startTime: { gte: filterDate },
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else { // we use month filter if date is passed and useDateFilter is false
|
} else {
|
||||||
|
// we use month filter if date is passed and useDateFilter is false to get all publishers with availabilities for the current month
|
||||||
if (fetchAvailabilities) {
|
if (fetchAvailabilities) {
|
||||||
// If no filter date, return all publishers's availabilities for currentMonthStart
|
// If no filter date, return all publishers's availabilities for currentMonthStart
|
||||||
whereClause["availabilities"] = {
|
whereClause["availabilities"] = {
|
||||||
@ -518,7 +660,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
// dayOfMonth: true,
|
// dayOfMonth: true,
|
||||||
// startTime: true,
|
// startTime: true,
|
||||||
// endTime: true,
|
// endTime: true,
|
||||||
// weekNr: true,
|
// weekOfMonth: true,
|
||||||
// type: true
|
// type: true
|
||||||
// },
|
// },
|
||||||
// where: {
|
// where: {
|
||||||
@ -609,6 +751,11 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd;
|
return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//if pub has availabilities for the current day
|
||||||
|
pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => {
|
||||||
|
return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filterDate && useDateFilter) {
|
if (filterDate && useDateFilter) {
|
||||||
@ -670,4 +817,22 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
const result = await prisma.assignment.updateMany({
|
||||||
|
where: {
|
||||||
|
publisherId: oldPublisherId,
|
||||||
|
shiftId: shiftId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
publisherId: newPublisherId,
|
||||||
|
isConfirmed: false,
|
||||||
|
isBySystem: true,
|
||||||
|
isMailSent: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
@ -89,12 +89,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const shifts = await prisma.shift.findMany({
|
const shifts = await prisma.shift.findMany({
|
||||||
where: {
|
where: {
|
||||||
isactive: true,
|
isActive: true,
|
||||||
|
isPublished: true,
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: fromDate,
|
gte: fromDate,
|
||||||
lt: toDate,
|
lt: toDate,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
startTime: 'asc',
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
assignments: {
|
assignments: {
|
||||||
where: {},
|
where: {},
|
||||||
@ -147,7 +151,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
.join(", "),
|
.join(", "),
|
||||||
};
|
};
|
||||||
|
|
||||||
groupedShifts[day][time].push(shiftSchedule);
|
if (shiftSchedule.names.length > 0) {
|
||||||
|
groupedShifts[day][time].push(shiftSchedule);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err + " " + JSON.stringify(shifts[i]));
|
console.log(err + " " + JSON.stringify(shifts[i]));
|
||||||
@ -188,8 +194,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
dayEvent.shifts.push(...groupedShifts[day][time]);
|
dayEvent.shifts.push(...groupedShifts[day][time]);
|
||||||
}
|
}
|
||||||
|
if (dayEvent) {
|
||||||
monthlySchedule.events.push(dayEvent);
|
monthlySchedule.events.push(dayEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = path.join(process.cwd(), 'public', 'content', 'output');
|
const outputPath = path.join(process.cwd(), 'public', 'content', 'output');
|
||||||
|
@ -68,7 +68,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
case "test":
|
case "test":
|
||||||
var data = prisma.shift.findMany({
|
var data = prisma.shift.findMany({
|
||||||
where: {
|
where: {
|
||||||
isactive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")");
|
console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")");
|
||||||
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":{"$eq":true}}`);
|
const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":{"$eq":true}}`);
|
||||||
|
|
||||||
//// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo);
|
//// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo);
|
||||||
//use filterPublishers from /pages/api/data/index.ts to get publishers with stats
|
//use filterPublishers from /pages/api/data/index.ts to get publishers with stats
|
||||||
@ -178,7 +178,8 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
|
|||||||
shiftNr++;
|
shiftNr++;
|
||||||
const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0");
|
const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0");
|
||||||
shiftAssignments = [];
|
shiftAssignments = [];
|
||||||
console.log("[shift " + shiftNr + "] " + __shiftName);
|
let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime();
|
||||||
|
console.log("[shift " + shiftNr + "] " + __shiftName + ", transport: " + (isTransportRequired ? "yes" : "no") + ", " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString() + " (end time: " + endTime.toLocaleTimeString() + ", " + event.shiftDuration + " min)");
|
||||||
|
|
||||||
if (autoFill || copyFromPreviousMonth) {
|
if (autoFill || copyFromPreviousMonth) {
|
||||||
// ###########################################
|
// ###########################################
|
||||||
@ -307,7 +308,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
|
|||||||
assignments: 'true',
|
assignments: 'true',
|
||||||
availabilities: 'true',
|
availabilities: 'true',
|
||||||
date: common.getISODateOnly(shiftStart),
|
date: common.getISODateOnly(shiftStart),
|
||||||
select: 'id,firstName,lastName,isactive,desiredShiftsPerMonth'
|
select: 'id,firstName,lastName,isActive,desiredShiftsPerMonth'
|
||||||
});
|
});
|
||||||
let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data;
|
let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data;
|
||||||
let availablePublishers = allAvailablePublishers;
|
let availablePublishers = allAvailablePublishers;
|
||||||
@ -395,6 +396,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
|
|||||||
startTime: shiftStart,
|
startTime: shiftStart,
|
||||||
endTime: shiftEnd,
|
endTime: shiftEnd,
|
||||||
name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(),
|
name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(),
|
||||||
|
requiresTransport: isTransportRequired,
|
||||||
cartEvent: {
|
cartEvent: {
|
||||||
connect: {
|
connect: {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
@ -442,7 +444,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
|
|||||||
}
|
}
|
||||||
|
|
||||||
//create shifts using API
|
//create shifts using API
|
||||||
// const { data: createdShifts } = await axios.post(`${process.env.NEXTAUTH_URL}/api/data/shifts`, shiftsToCreate);
|
// const { data: createdShifts } = await axios.post(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/shifts`, shiftsToCreate);
|
||||||
//const { data: allshifts } = await axios.get(`/api/data/shifts`);
|
//const { data: allshifts } = await axios.get(`/api/data/shifts`);
|
||||||
return {}; //allshifts;
|
return {}; //allshifts;
|
||||||
|
|
||||||
@ -558,7 +560,7 @@ async function DeleteSchedule(axios: Axios, date: Date, forDay: Boolean | undefi
|
|||||||
|
|
||||||
async function CreateCalendarForUser(eventId: string | string[] | undefined) {
|
async function CreateCalendarForUser(eventId: string | string[] | undefined) {
|
||||||
try {
|
try {
|
||||||
CAL.authorizeNew();
|
//CAL.authorizeNew();
|
||||||
CAL.createEvent(eventId);
|
CAL.createEvent(eventId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -614,12 +616,12 @@ async function ImportShiftsFromDocx(axios: Axios) {
|
|||||||
|
|
||||||
// prisma.publisher.findMany({
|
// prisma.publisher.findMany({
|
||||||
// where: {
|
// where: {
|
||||||
// isactive: true,
|
// isActive: true,
|
||||||
// },
|
// },
|
||||||
// include: {
|
// include: {
|
||||||
// availabilities: {
|
// availabilities: {
|
||||||
// where: {
|
// where: {
|
||||||
// isactive: true,
|
// isActive: true,
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
// assignments: {
|
// assignments: {
|
||||||
|
@ -30,7 +30,7 @@ export default function AvPage({ initialItems, id }: IProps) {
|
|||||||
date: new Date(item.startTime),
|
date: new Date(item.startTime),
|
||||||
start: new Date(item.startTime),
|
start: new Date(item.startTime),
|
||||||
end: new Date(item.endTime),
|
end: new Date(item.endTime),
|
||||||
isactive: item.isactive,
|
isActive: item.isActive,
|
||||||
publisherId: item.publisher.id,
|
publisherId: item.publisher.id,
|
||||||
dayOfMonth: item.dayOfMonth,
|
dayOfMonth: item.dayOfMonth,
|
||||||
dayOfWeek: item.dayOfWeek,
|
dayOfWeek: item.dayOfWeek,
|
||||||
@ -66,9 +66,9 @@ export default function AvPage({ initialItems, id }: IProps) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{initialItems?.map((item: Availability) => (
|
{initialItems?.map((item: Availability) => (
|
||||||
<tr key={item.id} className={item.isactive ? "" : "text-gray-300"}>
|
<tr key={item.id} className={item.isActive ? "" : "text-gray-300"}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap ">
|
<td className="px-6 py-4 whitespace-nowrap ">
|
||||||
{item.id} {item.isactive}
|
{item.id} {item.isActive}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
{item.publisher.lastName}, {item.publisher.firstName}
|
{item.publisher.lastName}, {item.publisher.firstName}
|
||||||
@ -133,19 +133,19 @@ export const getServerSideProps = async (context) => {
|
|||||||
|
|
||||||
const role = session?.user.role;
|
const role = session?.user.role;
|
||||||
console.log("server role: " + role);
|
console.log("server role: " + role);
|
||||||
var queryUrl = process.env.NEXTAUTH_URL + "/api/data/availabilities?select=id,name,isactive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id";
|
var queryUrl = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities?select=id,name,isActive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id";
|
||||||
if (role === UserRole.USER || context.query.my) {
|
if (role === UserRole.USER || context.query.my) {
|
||||||
queryUrl += `&where={"publisherId":"${session?.user.id}"}`;
|
queryUrl += `&where={"publisherId":"${session?.user.id}"}`;
|
||||||
} else if (role == UserRole.ADMIN) {
|
} else if (role == UserRole.ADMIN) {
|
||||||
if (context.query.id) {
|
if (context.query.id) {
|
||||||
queryUrl += `&where={"publisherId":"${context.query.id}"}`;
|
queryUrl += `&where={"publisherId":"${context.query.id}"}`;
|
||||||
} else {
|
} else {
|
||||||
queryUrl += `&where={"isactive":true}`;
|
queryUrl += `&where={"isActive":true}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var resp = await axios.get(
|
var resp = await axios.get(
|
||||||
queryUrl
|
queryUrl
|
||||||
// process.env.NEXTAUTH_URL + "/api/data/availabilities?include=publisher",
|
// process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities?include=publisher",
|
||||||
, { decompress: true });
|
, { decompress: true });
|
||||||
var items = resp.data;
|
var items = resp.data;
|
||||||
console.log("got " + items.length + " availabilities");
|
console.log("got " + items.length + " availabilities");
|
||||||
|
@ -31,7 +31,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { data: item } = await axios.get(
|
const { data: item } = await axios.get(
|
||||||
process.env.NEXTAUTH_URL + "/api/data/availabilities/" + context.params.id
|
process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities/" + context.params.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -14,6 +14,7 @@ const common = require('src/helpers/common');
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import ProtectedRoute from '../../../components/protectedRoute';
|
import ProtectedRoute from '../../../components/protectedRoute';
|
||||||
import ConfirmationModal from '../../../components/ConfirmationModal';
|
import ConfirmationModal from '../../../components/ConfirmationModal';
|
||||||
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
|
|
||||||
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
|
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
|
|
||||||
|
|
||||||
const [allShifts, setAllShifts] = useState(initialShifts);
|
const [allShifts, setAllShifts] = useState(initialShifts);
|
||||||
|
const [isPublished, setIsPublished] = useState(() => initialShifts.some(shift => shift.isPublished));
|
||||||
const [value, onChange] = useState<Date>(new Date());
|
const [value, onChange] = useState<Date>(new Date());
|
||||||
const [shifts, setShifts] = React.useState([]);
|
const [shifts, setShifts] = React.useState([]);
|
||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
@ -99,7 +101,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
|
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
|
||||||
if (isCheckboxChecked) {
|
if (isCheckboxChecked) {
|
||||||
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
|
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
|
||||||
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
|
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
||||||
setAvailablePubs(availablePubsForDate);
|
setAvailablePubs(availablePubsForDate);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -107,7 +109,14 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
try {
|
try {
|
||||||
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
|
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
|
||||||
setShifts(shiftsForDate);
|
setShifts(shiftsForDate);
|
||||||
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
|
setIsPublished(shiftsForDate.some(shift => shift.isPublished));
|
||||||
|
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
||||||
|
|
||||||
|
availablePubsForDate.forEach(pub => {
|
||||||
|
pub.canTransport = pub.availabilities.some(av =>
|
||||||
|
av.isWithTransportIn || av.isWithTransportOut
|
||||||
|
);
|
||||||
|
});
|
||||||
//remove availabilities that are isFromPreviousAssignment or from previous month for each publisher
|
//remove availabilities that are isFromPreviousAssignment or from previous month for each publisher
|
||||||
// availablePubsForDate = availablePubsForDate.map(pub => {
|
// availablePubsForDate = availablePubsForDate.map(pub => {
|
||||||
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
|
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
|
||||||
@ -134,30 +143,39 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
const handleShiftSelection = (selectedShift) => {
|
const handleShiftSelection = (selectedShift) => {
|
||||||
setSelectedShiftId(selectedShift.id);
|
setSelectedShiftId(selectedShift.id);
|
||||||
const updatedPubs = availablePubs.map(pub => {
|
const updatedPubs = availablePubs.map(pub => {
|
||||||
const isAvailableForShift = pub.availabilities.some(avail =>
|
const av = pub.availabilities?.find(avail =>
|
||||||
avail.startTime <= selectedShift.startTime
|
avail.startTime <= selectedShift.startTime
|
||||||
&& avail.endTime >= selectedShift.endTime
|
&& avail.endTime >= selectedShift.endTime
|
||||||
&& avail.isFromPreviousAssignment == false
|
|
||||||
);
|
);
|
||||||
|
if (av) {
|
||||||
|
pub.isAvailableForShift = true;
|
||||||
|
pub.canTransport = av.isWithTransportIn || av.isWithTransportOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const isAvailableForShift = pub.availabilities.some(avail =>
|
||||||
|
// avail.startTime <= selectedShift.startTime
|
||||||
|
// && avail.endTime >= selectedShift.endTime
|
||||||
|
// && avail.isFromPreviousAssignment == false
|
||||||
|
// );
|
||||||
const isAvailableForShiftWithPrevious = pub.availabilities.some(avail =>
|
const isAvailableForShiftWithPrevious = pub.availabilities.some(avail =>
|
||||||
avail.startTime <= selectedShift.startTime
|
avail.startTime <= selectedShift.startTime
|
||||||
&& avail.endTime >= selectedShift.endTime
|
&& avail.endTime >= selectedShift.endTime
|
||||||
);
|
);
|
||||||
//! console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`);
|
// //! console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`);
|
||||||
//// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime));
|
// //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime));
|
||||||
//// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id)));
|
// //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id)));
|
||||||
|
|
||||||
const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', ');
|
// const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', ');
|
||||||
//! console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`);
|
// //! console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`);
|
||||||
return { ...pub, isAvailableForShift, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id };
|
return { ...pub, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments,
|
// Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments,
|
||||||
// currentMonthAssignments and previousMonthAssignments properties
|
// currentMonthAssignments and previousMonthAssignments properties
|
||||||
// Sort publishers based on availability and then by assignment counts.
|
// Sort publishers based on availability and then by assignment counts.
|
||||||
const sortedPubs = updatedPubs.sort((a, b) => {
|
const sortedPubs = updatedPubs.sort((a, b) => {
|
||||||
if (a.isactive !== b.isactive) {
|
if (a.isActive !== b.isActive) {
|
||||||
return a.isactive ? -1 : 1;
|
return a.isActive ? -1 : 1;
|
||||||
}
|
}
|
||||||
// First, sort by isselected.
|
// First, sort by isselected.
|
||||||
if (a.isSelected !== b.isSelected) {
|
if (a.isSelected !== b.isSelected) {
|
||||||
@ -339,7 +357,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
const newAssignment = {
|
const newAssignment = {
|
||||||
publisher: { connect: { id: publisher.id } },
|
publisher: { connect: { id: publisher.id } },
|
||||||
shift: { connect: { id: shiftId } },
|
shift: { connect: { id: shiftId } },
|
||||||
isactive: true,
|
isActive: true,
|
||||||
isConfirmed: true
|
isConfirmed: true
|
||||||
};
|
};
|
||||||
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
||||||
@ -495,10 +513,26 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const togglePublished = async () => {
|
||||||
|
try {
|
||||||
|
const publishState = !isPublished; // Toggle the state
|
||||||
|
const isPublishedParam = publishState ? 'true' : 'fasle';
|
||||||
|
await axiosInstance.get(`/api/?action=updateShifts&isPublished=${isPublishedParam}&date=${common.getISODateOnly(value)}`);
|
||||||
|
setIsPublished(publishState); // Update state based on the action
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
|
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
|
||||||
|
async function copyOldAvailabilities(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||||||
|
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -534,6 +568,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
}}
|
}}
|
||||||
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
|
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
className={`button btn m-2 ${isPublished ? 'hover:bg-gray-100 bg-yellow-500' : 'hover:bg-red-300 bg-blue-400'}`}
|
||||||
|
onClick={togglePublished}>
|
||||||
|
<i className={`fas ${isPublished ? 'fa-check' : 'fa-close'} mr-2`}></i>
|
||||||
|
{isPublished ? "Скрий" : "Публикувай"} графика (м.)
|
||||||
|
</button>
|
||||||
<div className="relative inline-block text-left">
|
<div className="relative inline-block text-left">
|
||||||
<button
|
<button
|
||||||
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
|
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
|
||||||
@ -575,6 +615,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
|
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
|
||||||
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
|
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
|
||||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
|
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
|
||||||
|
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={copyOldAvailabilities}><i className="fas fa-copy mr-2"></i> Прехвърли предпочитанията</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -653,7 +694,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
// Determine border class if selected
|
// Determine border class if selected
|
||||||
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
|
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
|
||||||
// Determine opacity class
|
// Determine opacity class
|
||||||
const activeOpacityClass = pub.isactive ? '' : 'opacity-25';
|
const activeOpacityClass = pub.isActive ? '' : 'opacity-25';
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -664,6 +705,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
>
|
>
|
||||||
<span className={`text-gray-700 ${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
|
<span className={`text-gray-700 ${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
|
||||||
{pub.firstName} {pub.lastName}
|
{pub.firstName} {pub.lastName}
|
||||||
|
{pub.canTransport && (<LocalShippingIcon className="mx-2 text-gray-500" />)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex space-x-1 overflow-hidden">
|
<div className="flex space-x-1 overflow-hidden">
|
||||||
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
|
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
|
||||||
@ -849,9 +891,9 @@ import axiosServer from '../../../src/axiosServer';
|
|||||||
import { start } from 'repl';
|
import { start } from 'repl';
|
||||||
export const getServerSideProps = async (context) => {
|
export const getServerSideProps = async (context) => {
|
||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
const baseUrl = common.getBaseUrl();
|
// const baseUrl = common.getBaseUrl();
|
||||||
console.log('runtime BaseUrl: ' + baseUrl);
|
// console.log('runtime BaseUrl: ' + baseUrl);
|
||||||
console.log('runtime NEXTAUTH_URL: ' + process.env.NEXTAUTH_URL);
|
console.log('runtime NEXT_PUBLIC_PUBLIC_URL: ' + process.env.NEXT_PUBLIC_PUBLIC_URL);
|
||||||
console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
|
console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
|
||||||
|
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
@ -861,20 +903,20 @@ export const getServerSideProps = async (context) => {
|
|||||||
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
|
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
|
||||||
|
|
||||||
const prismaClient = common.getPrismaClient();
|
const prismaClient = common.getPrismaClient();
|
||||||
// let events = await prismaClient.cartEvent.findMany({ where: { isactive: true } });
|
// let events = await prismaClient.cartEvent.findMany({ where: { isActive: true } });
|
||||||
// events = events.map(event => ({
|
// events = events.map(event => ({
|
||||||
// ...event,
|
// ...event,
|
||||||
// // Convert Date objects to ISO strings
|
// // Convert Date objects to ISO strings
|
||||||
// startTime: event.startTime.toISOString(),
|
// startTime: event.startTime.toISOString(),
|
||||||
// endTime: event.endTime.toISOString(),
|
// endTime: event.endTime.toISOString(),
|
||||||
// }));
|
// }));
|
||||||
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":true}`);
|
const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":true}`);
|
||||||
//const { data: shifts } = await axios.get(url);
|
//const { data: shifts } = await axios.get(url);
|
||||||
|
|
||||||
// get all shifts for the month, including assigments
|
// get all shifts for the month, including assigments
|
||||||
let shifts = await prismaClient.shift.findMany({
|
let shifts = await prismaClient.shift.findMany({
|
||||||
where: {
|
where: {
|
||||||
isactive: true,
|
isActive: true,
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: firstDayOfMonth,
|
gte: firstDayOfMonth,
|
||||||
//lt: lastDayOfMonth
|
//lt: lastDayOfMonth
|
||||||
|
@ -7,9 +7,9 @@ export const getServerSideProps = async (context) => {
|
|||||||
console.log("edit page getServerSideProps");
|
console.log("edit page getServerSideProps");
|
||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
const { id } = context.query;
|
const { id } = context.query;
|
||||||
const { data } = await axios.get(`${process.env.NEXTAUTH_URL}/api/data/cartevents/` + id);
|
const { data } = await axios.get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/cartevents/` + id);
|
||||||
const locations = await axios
|
const locations = await axios
|
||||||
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
|
.get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations?select=id,name`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log("locations: " + JSON.stringify(res.data));
|
console.log("locations: " + JSON.stringify(res.data));
|
||||||
return res.data;
|
return res.data;
|
||||||
|
@ -69,7 +69,7 @@ export default function CartEventPage({ items, locations }: ICartEventPageProps)
|
|||||||
{item.shiftDuration}
|
{item.shiftDuration}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
{item.isactive ? "Yes" : "No"}
|
{item.isActive ? "Yes" : "No"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="button bg-blue-500 hover:bg-blue-700"
|
<button className="button bg-blue-500 hover:bg-blue-700"
|
||||||
|
@ -26,7 +26,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
|
|
||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
const locations = await axios
|
const locations = await axios
|
||||||
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
|
.get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations?select=id,name`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log("locations: " + JSON.stringify(res.data));
|
console.log("locations: " + JSON.stringify(res.data));
|
||||||
return res.data;
|
return res.data;
|
||||||
@ -41,7 +41,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
|
|
||||||
const { id } = context.query.id;
|
const { id } = context.query.id;
|
||||||
const { data: item } = await axiosInstance.get(
|
const { data: item } = await axiosInstance.get(
|
||||||
process.env.NEXTAUTH_URL + "/api/data/cartevents/" + context.params.id
|
process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/cartevents/" + context.params.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -99,11 +99,11 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
|
|
||||||
const { data: location } = await axios.get(
|
const { data: location } = await axios.get(
|
||||||
`${process.env.NEXTAUTH_URL}/api/data/locations/${context.params.id}`
|
`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations/${context.params.id}`
|
||||||
);
|
);
|
||||||
if (location.backupLocationId !== null) {
|
if (location.backupLocationId !== null) {
|
||||||
const { data: backupLocation } = await axios.get(
|
const { data: backupLocation } = await axios.get(
|
||||||
process.env.NEXTAUTH_URL + "/api/data/locations/" + location.backupLocationId
|
process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/locations/" + location.backupLocationId
|
||||||
);
|
);
|
||||||
location.backupLocationName = backupLocation.name;
|
location.backupLocationName = backupLocation.name;
|
||||||
location.backupLocationContent = backupLocation ? backupLocation.content : "";
|
location.backupLocationContent = backupLocation ? backupLocation.content : "";
|
||||||
|
@ -29,7 +29,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { data: item } = await axios.get(
|
const { data: item } = await axios.get(
|
||||||
process.env.NEXTAUTH_URL + "/api/data/locations/" + context.params.id
|
process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/locations/" + context.params.id
|
||||||
);
|
);
|
||||||
console.log(item) //this is the location object
|
console.log(item) //this is the location object
|
||||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||||
|
@ -32,7 +32,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { data: loc } = await axios.get(
|
const { data: loc } = await axios.get(
|
||||||
`${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
|
`${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id
|
||||||
);
|
);
|
||||||
console.log(location) //this is the location object
|
console.log(location) //this is the location object
|
||||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||||
|
@ -44,7 +44,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
props: {}
|
props: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
|
var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
|
||||||
console.log("GET PUBLISHER FROM:" + url)
|
console.log("GET PUBLISHER FROM:" + url)
|
||||||
const { data: item } = await axios.get(url);
|
const { data: item } = await axios.get(url);
|
||||||
|
|
||||||
|
@ -136,6 +136,7 @@ export default function ImportPage() {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
common.logger.debug("handleSave to: " + common.getBaseUrl());
|
common.logger.debug("handleSave to: " + common.getBaseUrl());
|
||||||
|
console.log("handleSave to: " + common.getBaseUrl());
|
||||||
const header = rawData[mode.headerRow];
|
const header = rawData[mode.headerRow];
|
||||||
for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher
|
for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher
|
||||||
//update status.info with current publisher
|
//update status.info with current publisher
|
||||||
@ -194,7 +195,7 @@ export default function ImportPage() {
|
|||||||
let personNames = names.join(' ');
|
let personNames = names.join(' ');
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isactive,type,availabilities";
|
const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isActive,type,availabilities";
|
||||||
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
|
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
|
||||||
let existingPublisher = responseByName.data[0];
|
let existingPublisher = responseByName.data[0];
|
||||||
if (!existingPublisher) {
|
if (!existingPublisher) {
|
||||||
@ -248,7 +249,7 @@ export default function ImportPage() {
|
|||||||
{ key: 'phone', value: phone },
|
{ key: 'phone', value: phone },
|
||||||
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
||||||
{ key: 'isTrained', value: isTrained },
|
{ key: 'isTrained', value: isTrained },
|
||||||
{ key: 'isactive', value: isActive },
|
{ key: 'isActive', value: isActive },
|
||||||
{ key: 'type', value: publisherType, parse: common.getPubTypeEnum }
|
{ key: 'type', value: publisherType, parse: common.getPubTypeEnum }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -286,7 +287,7 @@ export default function ImportPage() {
|
|||||||
phone,
|
phone,
|
||||||
firstName: names[0],
|
firstName: names[0],
|
||||||
lastName: names[1],
|
lastName: names[1],
|
||||||
isactive: isActive,
|
isActive: isActive,
|
||||||
isTrained,
|
isTrained,
|
||||||
desiredShiftsPerMonth,
|
desiredShiftsPerMonth,
|
||||||
});
|
});
|
||||||
@ -301,7 +302,7 @@ export default function ImportPage() {
|
|||||||
phone,
|
phone,
|
||||||
firstName: firstname,
|
firstName: firstname,
|
||||||
lastName: names[names.length - 1],
|
lastName: names[names.length - 1],
|
||||||
isactive: isActive,
|
isActive: isActive,
|
||||||
isTrained,
|
isTrained,
|
||||||
desiredShiftsPerMonth
|
desiredShiftsPerMonth
|
||||||
});
|
});
|
||||||
@ -382,7 +383,7 @@ export default function ImportPage() {
|
|||||||
|
|
||||||
common.logger.debug("processing availabilities for " + day.toLocaleDateString()); // Output: Sun Apr 17 2022 14:07:11 GMT+0300 (Eastern European Summer Time)
|
common.logger.debug("processing availabilities for " + day.toLocaleDateString()); // Output: Sun Apr 17 2022 14:07:11 GMT+0300 (Eastern European Summer Time)
|
||||||
common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday)
|
common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday)
|
||||||
const dayOfWeekName = common.getDayOfWeekNameEnEnum(day);
|
const dayOfWeekName = common.getDayOfWeekNameEnEnumForDate(day);
|
||||||
|
|
||||||
let dayOfMonth = day.getDate();
|
let dayOfMonth = day.getDate();
|
||||||
const name = `${names[0]} ${names[1]}`;
|
const name = `${names[0]} ${names[1]}`;
|
||||||
@ -530,7 +531,7 @@ export default function ImportPage() {
|
|||||||
weekOfMonth: weekNr, // Add the missing 'weekOfMonth'
|
weekOfMonth: weekNr, // Add the missing 'weekOfMonth'
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
isactive: true,
|
isActive: true,
|
||||||
type: AvailabilityType.OneTime,
|
type: AvailabilityType.OneTime,
|
||||||
isWithTransportIn: false, // Add the missing 'isWithTransport' property
|
isWithTransportIn: false, // Add the missing 'isWithTransport' property
|
||||||
isWithTransportOut: false, // Add the missing 'isWithTransport' property
|
isWithTransportOut: false, // Add the missing 'isWithTransport' property
|
||||||
|
@ -225,7 +225,7 @@ export default PublishersPage;
|
|||||||
export const getServerSideProps = async (context) => {
|
export const getServerSideProps = async (context) => {
|
||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
//ToDo: refactor all axios calls to use axiosInstance and this URL
|
//ToDo: refactor all axios calls to use axiosInstance and this URL
|
||||||
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isactive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
|
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isActive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
@ -4,18 +4,70 @@ import ProtectedRoute from '../../../components/protectedRoute';
|
|||||||
import { UserRole } from '@prisma/client';
|
import { UserRole } from '@prisma/client';
|
||||||
import axiosServer from '../../../src/axiosServer';
|
import axiosServer from '../../../src/axiosServer';
|
||||||
import common from '../../../src/helpers/common';
|
import common from '../../../src/helpers/common';
|
||||||
import ShiftsList from '../../../components/publisher/ShiftsList';
|
import Modal from 'components/Modal';
|
||||||
|
import ConfirmationModal from 'components/ConfirmationModal';
|
||||||
|
import PublisherSearchBox from '../../../components/publisher/PublisherSearchBox'; // Update the path
|
||||||
|
|
||||||
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../../src/helpers/const"
|
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../../src/helpers/const"
|
||||||
import { useSession, getSession } from 'next-auth/react';
|
import { useSession, getSession } from 'next-auth/react';
|
||||||
|
import axiosInstance from 'src/axiosSecure';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
|
|
||||||
export default function MySchedulePage({ assignments }) {
|
export default function MySchedulePage({ assignments }) {
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
|
||||||
|
const [useFilterDate, setUseFilterDate] = useState(true);
|
||||||
|
const [assignment, setАssignment] = useState(null);
|
||||||
|
const [newPublisher, setNewPublisher] = useState(null);
|
||||||
|
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
if (status === "loading") {
|
if (status === "loading") {
|
||||||
return <div className="flex justify-center items-center h-screen">Loading...</div>;
|
return <div className="flex justify-center items-center h-screen">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleReplaceInAssignment = () => {
|
||||||
|
// Add publisher as assignment logic
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setIsConfirmModalOpen(false);
|
||||||
|
console.log("publisher", newPublisher.firstName, " ", newPublisher.lastName, " set to shift ", assignment.shift.id);
|
||||||
|
//api.replaceInAssignment()
|
||||||
|
axiosInstance.post('/api/?action=replaceInAssignment', {
|
||||||
|
oldPublisherId: session.user.id,
|
||||||
|
newPublisherId: newPublisher.id,
|
||||||
|
shiftId: assignment.shift.id,
|
||||||
|
}).then(response => {
|
||||||
|
console.log("response", response);
|
||||||
|
//toast success and confirm the change
|
||||||
|
// refresh the page after toast is closed
|
||||||
|
toast.success("Промяната е записана!", {
|
||||||
|
onClose: () => {
|
||||||
|
window.location.reload();
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(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]}>
|
||||||
@ -23,7 +75,7 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
<h1 className="text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
<h1 className="text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{assignments && assignments.map((assignment) => (
|
{assignments && assignments.map((assignment) => (
|
||||||
<div key={assignment.dateStr} className="bg-white shadow overflow-hidden rounded-lg">
|
<div key={assignment.dateStr + assignments.indexOf(assignment)} className="bg-white shadow overflow-hidden rounded-lg">
|
||||||
<div className="px-4 py-5 sm:px-6">
|
<div className="px-4 py-5 sm:px-6">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">{assignment.dateStr}</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900">{assignment.dateStr}</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -31,21 +83,46 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
<dl>
|
<dl>
|
||||||
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
<dt className="text-sm font-medium text-gray-500">Час</dt>
|
<dt className="text-sm font-medium text-gray-500">Час</dt>
|
||||||
<dd className="mt-1 text-sm 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">
|
||||||
{GetTimeFormat(assignment.shift.startTime)} - {GetTimeFormat(assignment.shift.endTime)}
|
{GetTimeFormat(assignment.shift.startTime)} - {GetTimeFormat(assignment.shift.endTime)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">Смяна</dt>
|
||||||
|
<dd className="mt-1 text text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{assignment.shift.assignments.map((a, index) => {
|
||||||
|
return (
|
||||||
|
<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={{ marginLeft: '4px' }} />}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
<dt className="text-sm font-medium text-gray-500">Действия</dt>
|
<dt className="text-sm font-medium text-gray-500">Действия</dt>
|
||||||
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
<button
|
<button
|
||||||
className="mr-2 mb-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
className="mr-2 mb-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled"
|
||||||
onClick={() => AddToGoogleCalendar(assignment.id)}
|
disabled={true}
|
||||||
|
// disabled={assignment.isInGoogleCalendar}
|
||||||
|
onClick={() => SaveEventsInGoogleCalendar(assignment)}
|
||||||
>
|
>
|
||||||
Добави в календар
|
Добави в календар
|
||||||
</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="mr-2 mb-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
onClick={() => {
|
||||||
|
setАssignment(assignment);
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Избери Заместник
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="mr-2 mb-2 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"
|
||||||
onClick={() => searchReplacement(assignment.id)}
|
onClick={() => searchReplacement(assignment.id)}
|
||||||
>
|
>
|
||||||
Търси заместник
|
Търси заместник
|
||||||
@ -58,6 +135,32 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
forDate={new Date(assignment?.shift.startTime)}
|
||||||
|
useFilterDate={useFilterDate}
|
||||||
|
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
||||||
|
|
||||||
|
<PublisherSearchBox
|
||||||
|
selectedId={null}
|
||||||
|
isFocused={isModalOpen}
|
||||||
|
filterDate={useFilterDate && assignment ? new Date(assignment.shift.startTime) : null}
|
||||||
|
onChange={(publisher) => {
|
||||||
|
setIsConfirmModalOpen(true);
|
||||||
|
setNewPublisher(publisher);
|
||||||
|
}}
|
||||||
|
showAllAuto={true}
|
||||||
|
showSearch={true}
|
||||||
|
showList={false}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isConfirmModalOpen}
|
||||||
|
onClose={() => { setIsConfirmModalOpen(false); setNewPublisher(null); }}
|
||||||
|
onConfirm={handleReplaceInAssignment}
|
||||||
|
message="Това действие ще те замести в назначената ти смяна. Потвърждаваш ли, че заместника знае за тази промяна."
|
||||||
|
/>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
@ -80,14 +183,18 @@ export const getServerSideProps = async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prisma = common.getPrismaClient();
|
const prisma = common.getPrismaClient();
|
||||||
const publisher = await prisma.publisher.findMany({
|
const monthInfo = common.getMonthInfo(new Date());
|
||||||
|
//minus 1 day from the firstMonday to get the last Sunday
|
||||||
|
const lastSunday = new Date(monthInfo.firstMonday);
|
||||||
|
lastSunday.setDate(lastSunday.getDate() - 1);
|
||||||
|
const publisher = await prisma.publisher.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
assignments: {
|
assignments: {
|
||||||
some: {
|
some: {
|
||||||
shift: {
|
shift: {
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: new Date(),
|
gte: lastSunday,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -96,15 +203,29 @@ export const getServerSideProps = async (context) => {
|
|||||||
include: {
|
include: {
|
||||||
assignments: {
|
assignments: {
|
||||||
include: {
|
include: {
|
||||||
shift: true,
|
shift: {
|
||||||
|
include: {
|
||||||
|
assignments: {
|
||||||
|
include: {
|
||||||
|
publisher: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignments = publisher[0]?.assignments;
|
const assignments = publisher?.assignments || [];
|
||||||
|
|
||||||
const transformedAssignments = assignments.map(assignment => {
|
const transformedAssignments = assignments?.map(assignment => {
|
||||||
if (assignment.shift && assignment.shift.startTime) {
|
if (assignment.shift && assignment.shift.startTime) {
|
||||||
return {
|
return {
|
||||||
...assignment,
|
...assignment,
|
||||||
|
@ -47,7 +47,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
props: {}
|
props: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts";
|
var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts";
|
||||||
console.log("GET PUBLISHER FROM:" + url)
|
console.log("GET PUBLISHER FROM:" + url)
|
||||||
const { data } = await axios.get(url);
|
const { data } = await axios.get(url);
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ function ContactsPage({ publishers }) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{filteredPublishers.map((pub) => (
|
{filteredPublishers.map((pub) => (
|
||||||
<tr key={pub.id}>
|
<tr key={pub.id}>
|
||||||
<td className="border-b p-4 pl-8">{pub.firstName} {pub.lastName}</td>
|
<td className="border-b p-4 pl-8" title={pub.lastUpdate}>{pub.firstName} {pub.lastName}</td>
|
||||||
<td className="border-b p-4">
|
<td className="border-b p-4">
|
||||||
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
|
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
|
||||||
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
||||||
@ -74,16 +74,23 @@ export default ContactsPage;
|
|||||||
export const getServerSideProps = async (context) => {
|
export const getServerSideProps = async (context) => {
|
||||||
const dateStr = new Date().toISOString().split('T')[0];
|
const dateStr = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
let publishers = await filterPublishers('id,firstName,lastName,email,isactive,desiredShiftsPerMonth', "", new Date(), true, true, false);
|
let publishers = await filterPublishers('id,firstName,lastName,email,isActive,desiredShiftsPerMonth', "", new Date(), true, true, false);
|
||||||
|
|
||||||
// const axios = await axiosServer(context);
|
// const axios = await axiosServer(context);
|
||||||
// const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
|
// const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
||||||
|
|
||||||
// api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2Cisactive%2CdesiredShiftsPerMonth
|
// api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2CisActive%2CdesiredShiftsPerMonth
|
||||||
publishers.forEach(publisher => {
|
publishers.forEach(publisher => {
|
||||||
publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0;
|
publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0;
|
||||||
publisher.assignments = publisher.assignments || [];
|
publisher.assignments = publisher.assignments || [];
|
||||||
publisher.availabilities = publisher.availabilities || [];
|
publisher.availabilities = publisher.availabilities || [];
|
||||||
|
publisher.lastUpdate = publisher.availabilities.reduce((acc, curr) => curr.dateOfEntry > acc ? curr.dateOfEntry : acc, null);
|
||||||
|
if (publisher.lastUpdate) {
|
||||||
|
publisher.lastUpdate = common.getDateFormated(publisher.lastUpdate);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
publisher.lastUpdate = "Няма данни";
|
||||||
|
}
|
||||||
//serialize dates in publisher.assignments and publisher.availabilities
|
//serialize dates in publisher.assignments and publisher.availabilities
|
||||||
publisher.assignments.forEach(assignment => {
|
publisher.assignments.forEach(assignment => {
|
||||||
if (assignment.shift && assignment.shift.startTime) {
|
if (assignment.shift && assignment.shift.startTime) {
|
||||||
@ -95,10 +102,18 @@ export const getServerSideProps = async (context) => {
|
|||||||
if (availability.startTime) {
|
if (availability.startTime) {
|
||||||
availability.startTime = availability.startTime.toISOString();
|
availability.startTime = availability.startTime.toISOString();
|
||||||
availability.endTime = availability.endTime.toISOString();
|
availability.endTime = availability.endTime.toISOString();
|
||||||
|
if (availability.dateOfEntry) {
|
||||||
|
availability.dateOfEntry = availability.dateOfEntry.toISOString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
//remove availabilities that isFromPreviousAssignment
|
||||||
|
publisher.availabilities = publisher.availabilities.filter(availability => !availability.isFromPreviousAssignment);
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
//remove publishers without availabilities
|
||||||
|
publishers = publishers.filter(publisher => publisher.availabilities.length > 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
@ -32,7 +32,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
// const { data: loc } = await axiosInstance.get(
|
// const { data: loc } = await axiosInstance.get(
|
||||||
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
|
// `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// console.log(location) //this is the location object
|
// console.log(location) //this is the location object
|
||||||
|
@ -22,7 +22,7 @@ export default function Reports() {
|
|||||||
|
|
||||||
const deleteReport = (id) => {
|
const deleteReport = (id) => {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.delete(`api/data/reports/${id}`)
|
.delete(`/api/data/reports/${id}`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
toast.success("Успешно изтрит отчет");
|
toast.success("Успешно изтрит отчет");
|
||||||
// router.push("/cart/reports/list");
|
// router.push("/cart/reports/list");
|
||||||
@ -43,12 +43,12 @@ export default function Reports() {
|
|||||||
const { data } = await axiosInstance.get("/api/data/locations");
|
const { data } = await axiosInstance.get("/api/data/locations");
|
||||||
setLocations(data);
|
setLocations(data);
|
||||||
console.log(data);
|
console.log(data);
|
||||||
axiosInstance.get(`/api/data/reports`)
|
axiosInstance.get(`/api/data/reports?include=publisher,location`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
let reports = res.data;
|
// let reports = res.data;
|
||||||
reports.forEach((report) => {
|
// reports.forEach((report) => {
|
||||||
report.location = data.find((loc) => loc.id === report.locationId);
|
// report.location = data.find((loc) => loc.id === report.locationId);
|
||||||
});
|
// });
|
||||||
setReports(res.data);
|
setReports(res.data);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -74,10 +74,23 @@ export default function Reports() {
|
|||||||
Добави нов отчет
|
Добави нов отчет
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<label className="mr-4">
|
||||||
|
<input type="radio" name="reportType" value="ServiceReport" defaultChecked />
|
||||||
|
Отчети
|
||||||
|
</label>
|
||||||
|
<label className="mr-4">
|
||||||
|
<input type="radio" name="reportType" value="Experience" />
|
||||||
|
Случка
|
||||||
|
</label>
|
||||||
|
<label className="mr-4">
|
||||||
|
<input type="radio" name="reportType" value="Feedback" />
|
||||||
|
Отзиви
|
||||||
|
</label>
|
||||||
<div className="mt-4 w-full overflow-x-auto">
|
<div className="mt-4 w-full overflow-x-auto">
|
||||||
<table className="w-full table-auto">
|
<table className="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left">От</th>
|
||||||
<th className="px-4 py-2 text-left">Дата</th>
|
<th className="px-4 py-2 text-left">Дата</th>
|
||||||
<th className="px-4 py-2 text-left" >Място</th>
|
<th className="px-4 py-2 text-left" >Място</th>
|
||||||
<th className="px-4 py-2 text-left">Отчет</th>
|
<th className="px-4 py-2 text-left">Отчет</th>
|
||||||
@ -87,9 +100,10 @@ export default function Reports() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{reports.map((report) => (
|
{reports.map((report) => (
|
||||||
<tr key={report.id}>
|
<tr key={report.id}>
|
||||||
<td className="border px-4 py-2">{common.getDateFormated(new Date(report.date))}</td>
|
<td className="border px-2 py-2">{report.publisher.firstName + " " + report.publisher.lastName}</td>
|
||||||
<td className="border px-4 py-2">{report.location?.name}</td>
|
<td className="border px-2 py-2">{common.getDateFormated(new Date(report.date))}</td>
|
||||||
<td className="border px-4 py-2">
|
<td className="border px-2 py-2">{report.location?.name}</td>
|
||||||
|
<td className="border px-2 py-2">
|
||||||
{(report.experienceInfo === null || report.experienceInfo === "")
|
{(report.experienceInfo === null || report.experienceInfo === "")
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
@ -99,12 +113,19 @@ export default function Reports() {
|
|||||||
Клипове: {report.videoCount} <br />
|
Клипове: {report.videoCount} <br />
|
||||||
Адреси / Телефони: {report.returnVisitInfoCount} <br />
|
Адреси / Телефони: {report.returnVisitInfoCount} <br />
|
||||||
</>
|
</>
|
||||||
|
) : (report.placementCount > 0) ? (
|
||||||
|
<>
|
||||||
|
<div><strong>Отзив</strong></div>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div><strong>Случка</strong></div>
|
<div><strong>Случка</strong></div>
|
||||||
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
|
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td className="border px-4 py-2">
|
<td className="border px-4 py-2">
|
||||||
|
|
||||||
@ -122,8 +143,8 @@ export default function Reports() {
|
|||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
</div >
|
</div >
|
||||||
</ProtectedRoute>
|
</ProtectedRoute >
|
||||||
</Layout>
|
</Layout >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// const { data: loc } = await axiosInstance.get(
|
// const { data: loc } = await axiosInstance.get(
|
||||||
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
|
// `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id
|
||||||
// );
|
// );
|
||||||
// console.log(location) //this is the location object
|
// console.log(location) //this is the location object
|
||||||
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||||
|
@ -24,7 +24,7 @@ const ContactsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div > */
|
</div > */
|
||||||
}
|
}
|
||||||
{/* <a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
{/* <a href="https://t.me/mwHitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
||||||
<img src="styles/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
<img src="styles/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
||||||
<span className="align-middle">Телеграм</span>
|
<span className="align-middle">Телеграм</span>
|
||||||
</a> */}
|
</a> */}
|
||||||
|
@ -86,7 +86,7 @@ async function getAvailabilities(userId) {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
isactive: true,
|
isActive: true,
|
||||||
isFromPreviousAssignment: true,
|
isFromPreviousAssignment: true,
|
||||||
dayofweek: true,
|
dayofweek: true,
|
||||||
dayOfMonth: true,
|
dayOfMonth: true,
|
||||||
@ -121,7 +121,7 @@ async function getAvailabilities(userId) {
|
|||||||
shiftId Int
|
shiftId Int
|
||||||
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
||||||
publisherId String
|
publisherId String
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isConfirmed Boolean @default(false)
|
isConfirmed Boolean @default(false)
|
||||||
isWithTransport Boolean @default(false)
|
isWithTransport Boolean @default(false)
|
||||||
Report Report[]
|
Report Report[]
|
||||||
@ -133,7 +133,7 @@ async function getAvailabilities(userId) {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
isTentative: true,
|
isBySystem: true,
|
||||||
isConfirmed: true,
|
isConfirmed: true,
|
||||||
isWithTransport: true,
|
isWithTransport: true,
|
||||||
shift: {
|
shift: {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -9,10 +9,19 @@ const PDFViewerPage = ({ pdfFiles }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<h1 className="text-3xl font-bold">Разрешителни</h1>
|
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
|
||||||
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
||||||
|
{/* <p className="p-1">
|
||||||
|
{pdfFiles.map((file, index) => (
|
||||||
|
<p className="p-2">
|
||||||
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||||
|
Свали: {file.name}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</p> */}
|
||||||
{pdfFiles.map((file, index) => (
|
{pdfFiles.map((file, index) => (
|
||||||
|
|
||||||
// <React.Fragment key={file.name}>
|
// <React.Fragment key={file.name}>
|
||||||
// {index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
|
// {index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
|
||||||
// <a
|
// <a
|
||||||
@ -23,13 +32,18 @@ const PDFViewerPage = ({ pdfFiles }) => {
|
|||||||
// {file.name}
|
// {file.name}
|
||||||
// </a>
|
// </a>
|
||||||
// </React.Fragment>
|
// </React.Fragment>
|
||||||
<div style={{ width: 'calc(100% - 1rem)', height: '100%', margin: '0 0' }}>
|
<> <p className="pt-2">
|
||||||
< object data={file.url} type="application/pdf" style={{ width: '100%', height: '100%' }}>
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||||
<p>Вашият браузър не поддържа PDFs файлове. Моля свалете файла за да го разгледате: <a href={file.url}>Свали {file.name}</a>.</p>
|
Свали: {file.name}
|
||||||
<p>Your browser does not support PDFs. Please download the PDF to view it: <a href={file.url}> {file.name}</a>.</p>
|
</a>
|
||||||
</object>
|
</p>
|
||||||
</div>
|
<div style={{ width: 'calc(100% - 1rem)', height: '100%' }} className='py-2'>
|
||||||
|
< object data={file.url} type="application/pdf" style={{ width: '100%', height: '100%' }}>
|
||||||
|
<p>Вашият браузър не поддържа PDFs файлове. Моля свалете файла за да го разгледате: <a href={file.url}>Свали {file.name}</a>.</p>
|
||||||
|
<p>Your browser does not support PDFs. Please download the PDF to view it: <a href={file.url}> {file.name}</a>.</p>
|
||||||
|
</object>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Layout >
|
</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;
|
28
prisma/administrative_scripts/fix_availability_dayofweek.sql
Normal file
28
prisma/administrative_scripts/fix_availability_dayofweek.sql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-- find
|
||||||
|
SELECT *
|
||||||
|
FROM availability
|
||||||
|
WHERE dayofweek = "Thursday"
|
||||||
|
AND DAYOFWEEK(startTime) <> 5;
|
||||||
|
|
||||||
|
|
||||||
|
--fix
|
||||||
|
|
||||||
|
UPDATE availability
|
||||||
|
SET dayofweek = CASE DAYOFWEEK(startTime)
|
||||||
|
WHEN 1 THEN 'Sunday'
|
||||||
|
WHEN 2 THEN 'Monday'
|
||||||
|
WHEN 3 THEN 'Tuesday'
|
||||||
|
WHEN 4 THEN 'Wednesday'
|
||||||
|
WHEN 5 THEN 'Thursday'
|
||||||
|
WHEN 6 THEN 'Friday'
|
||||||
|
WHEN 7 THEN 'Saturday'
|
||||||
|
END
|
||||||
|
WHERE dayofweek != CASE DAYOFWEEK(startTime)
|
||||||
|
WHEN 1 THEN 'Sunday'
|
||||||
|
WHEN 2 THEN 'Monday'
|
||||||
|
WHEN 3 THEN 'Tuesday'
|
||||||
|
WHEN 4 THEN 'Wednesday'
|
||||||
|
WHEN 5 THEN 'Thursday'
|
||||||
|
WHEN 6 THEN 'Friday'
|
||||||
|
WHEN 7 THEN 'Saturday'
|
||||||
|
END;
|
@ -13,7 +13,7 @@ export const publisherSelectWithAvCount = {
|
|||||||
select: {
|
select: {
|
||||||
availability: {
|
availability: {
|
||||||
where: {
|
where: {
|
||||||
isactive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export const publisherSelect = {
|
|||||||
// endTime: true,
|
// endTime: true,
|
||||||
// dayOfMonth: true,
|
// dayOfMonth: true,
|
||||||
// dayofweek: true,
|
// dayofweek: true,
|
||||||
// isactive: true,
|
// isActive: true,
|
||||||
// count: 'Availability_count'
|
// count: 'Availability_count'
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
@ -5,7 +5,7 @@ CREATE TABLE `Publisher` (
|
|||||||
`lastName` VARCHAR(191) NOT NULL,
|
`lastName` VARCHAR(191) NOT NULL,
|
||||||
`email` VARCHAR(191) NOT NULL,
|
`email` VARCHAR(191) NOT NULL,
|
||||||
`phone` VARCHAR(191) NULL,
|
`phone` VARCHAR(191) NULL,
|
||||||
`isactive` BOOLEAN NOT NULL DEFAULT true,
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
`age` INTEGER NULL,
|
`age` INTEGER NULL,
|
||||||
|
|
||||||
UNIQUE INDEX `Publisher_email_key`(`email`),
|
UNIQUE INDEX `Publisher_email_key`(`email`),
|
||||||
@ -41,7 +41,7 @@ CREATE TABLE `Shift` (
|
|||||||
`name` VARCHAR(191) NOT NULL,
|
`name` VARCHAR(191) NOT NULL,
|
||||||
`startTime` DATETIME(3) NOT NULL,
|
`startTime` DATETIME(3) NOT NULL,
|
||||||
`endTime` DATETIME(3) NOT NULL,
|
`endTime` DATETIME(3) NOT NULL,
|
||||||
`isactive` BOOLEAN NOT NULL DEFAULT true,
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
`requiresTransport` BOOLEAN NOT NULL DEFAULT false,
|
`requiresTransport` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
@ -52,7 +52,7 @@ CREATE TABLE `Location` (
|
|||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`name` VARCHAR(191) NOT NULL,
|
`name` VARCHAR(191) NOT NULL,
|
||||||
`address` VARCHAR(191) NOT NULL,
|
`address` VARCHAR(191) NOT NULL,
|
||||||
`isactive` BOOLEAN NOT NULL DEFAULT true,
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
|
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE `Availability` ADD COLUMN `isactive` BOOLEAN NOT NULL DEFAULT true;
|
ALTER TABLE `Availability` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE `CartEvent` ADD COLUMN `isactive` BOOLEAN NOT NULL DEFAULT true,
|
ALTER TABLE `CartEvent` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
ADD COLUMN `locationId` INTEGER NOT NULL,
|
ADD COLUMN `locationId` INTEGER NOT NULL,
|
||||||
ADD COLUMN `shiftDuration` INTEGER NOT NULL;
|
ADD COLUMN `shiftDuration` INTEGER NOT NULL;
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ CREATE TABLE `Assignment` (
|
|||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`shiftId` INTEGER NOT NULL,
|
`shiftId` INTEGER NOT NULL,
|
||||||
`publisherId` INTEGER NOT NULL,
|
`publisherId` INTEGER NOT NULL,
|
||||||
`isactive` BOOLEAN NOT NULL DEFAULT true,
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
Warnings:
|
Warnings:
|
||||||
|
|
||||||
- You are about to drop the column `isactive` on the `assignment` table. All the data in the column will be lost.
|
- You are about to drop the column `isActive` on the `assignment` table. All the data in the column will be lost.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE `Assignment` DROP COLUMN `isactive`,
|
ALTER TABLE `Assignment` DROP COLUMN `isActive`,
|
||||||
ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false;
|
ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
|
21
prisma/migrations/20240325214807_misc_renames/migration.sql
Normal file
21
prisma/migrations/20240325214807_misc_renames/migration.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `isTentative` on the `Assignment` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Assignment`
|
||||||
|
ADD COLUMN `isBySystem` BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Depending on your DBMS, you might need to execute one statement at a time.
|
||||||
|
-- Especially, the UPDATE statement should be run separately.
|
||||||
|
UPDATE `Assignment` SET `isBySystem` = isTentative;
|
||||||
|
|
||||||
|
-- Drop the isTentative column
|
||||||
|
ALTER TABLE `Assignment` DROP COLUMN `isTentative`;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Report`
|
||||||
|
ADD COLUMN `type` ENUM('ServiceReport', 'Experience', 'Feedback_Problem', 'Feedback_Suggestion', 'Feedback') NOT NULL DEFAULT 'ServiceReport';
|
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Availability` ADD COLUMN `parentAvailabilityId` INTEGER NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_parentAvailabilityId_fkey` FOREIGN KEY (`parentAvailabilityId`) REFERENCES `Availability`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Publisher` ADD COLUMN `isSubscribedToCoverMe` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN `isSubscribedToReminders` BOOLEAN NOT NULL DEFAULT false;
|
@ -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 {
|
||||||
@ -81,13 +81,21 @@ enum PublisherType {
|
|||||||
SpecialPioneer_Missionary
|
SpecialPioneer_Missionary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ReportType {
|
||||||
|
ServiceReport
|
||||||
|
Experience
|
||||||
|
Feedback_Problem
|
||||||
|
Feedback_Suggestion
|
||||||
|
Feedback
|
||||||
|
}
|
||||||
|
|
||||||
model Publisher {
|
model Publisher {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
firstName String
|
firstName String
|
||||||
lastName String
|
lastName String
|
||||||
email String @unique
|
email String @unique
|
||||||
phone String?
|
phone String?
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isImported Boolean @default(false)
|
isImported Boolean @default(false)
|
||||||
isTrained Boolean @default(false)
|
isTrained Boolean @default(false)
|
||||||
age Int?
|
age Int?
|
||||||
@ -98,20 +106,21 @@ model Publisher {
|
|||||||
userId String? @unique
|
userId String? @unique
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
role UserRole @default(USER)
|
role UserRole @default(USER)
|
||||||
desiredShiftsPerMonth Int @default(4)
|
desiredShiftsPerMonth Int @default(4)
|
||||||
isMale Boolean @default(true)
|
isMale Boolean @default(true)
|
||||||
isNameForeign Boolean @default(false)
|
isNameForeign Boolean @default(false)
|
||||||
|
isSubscribedToCoverMe Boolean @default(false)
|
||||||
familyHeadId String? // Optional familyHeadId for each family member
|
isSubscribedToReminders Boolean @default(false)
|
||||||
familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
|
familyHeadId String? // Optional familyHeadId for each family member
|
||||||
familyMembers Publisher[] @relation("FamilyMember")
|
familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
|
||||||
alwaysAsFamily Boolean? @default(false) //NEW v1.0.1 // New field to indicate if the publisher always wants to be assigned with the family
|
familyMembers Publisher[] @relation("FamilyMember")
|
||||||
type PublisherType @default(Publisher)
|
alwaysAsFamily Boolean? @default(false) //NEW v1.0.1 // New field to indicate if the publisher always wants to be assigned with the family
|
||||||
town String?
|
type PublisherType @default(Publisher)
|
||||||
comments String?
|
town String?
|
||||||
reports Report[]
|
comments String?
|
||||||
Message Message[]
|
reports Report[]
|
||||||
|
Message Message[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Availability {
|
model Availability {
|
||||||
@ -124,7 +133,7 @@ model Availability {
|
|||||||
weekOfMonth Int?
|
weekOfMonth Int?
|
||||||
startTime DateTime
|
startTime DateTime
|
||||||
endTime DateTime
|
endTime DateTime
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
type AvailabilityType @default(Weekly)
|
type AvailabilityType @default(Weekly)
|
||||||
isWithTransportIn Boolean @default(false)
|
isWithTransportIn Boolean @default(false)
|
||||||
isWithTransportOut Boolean @default(false)
|
isWithTransportOut Boolean @default(false)
|
||||||
@ -133,7 +142,10 @@ model Availability {
|
|||||||
repeatWeekly Boolean? // New field to indicate weekly repetition // until now dayofweek was used for repetition when dayOfMonth is null
|
repeatWeekly Boolean? // New field to indicate weekly repetition // until now dayofweek was used for repetition when dayOfMonth is null
|
||||||
repeatFrequency Int? // New field to indicate repetition frequency
|
repeatFrequency Int? // New field to indicate repetition frequency
|
||||||
endDate DateTime? // New field for the end date of repetition
|
endDate DateTime? // New field for the end date of repetition
|
||||||
dateOfEntry DateTime? //NEW v1.0.1 trade storage for intuintivity
|
dateOfEntry DateTime? //NEW v1.0.1
|
||||||
|
parentAvailabilityId Int?
|
||||||
|
parentAvailability Availability? @relation("ParentAvailability", fields: [parentAvailabilityId], references: [id])
|
||||||
|
ChildAvailabilities Availability[] @relation("ParentAvailability")
|
||||||
}
|
}
|
||||||
|
|
||||||
model CartEvent {
|
model CartEvent {
|
||||||
@ -143,7 +155,7 @@ model CartEvent {
|
|||||||
shiftDuration Int
|
shiftDuration Int
|
||||||
shifts Shift[]
|
shifts Shift[]
|
||||||
dayofweek DayOfWeek
|
dayofweek DayOfWeek
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
location Location @relation(fields: [locationId], references: [id])
|
location Location @relation(fields: [locationId], references: [id])
|
||||||
locationId Int
|
locationId Int
|
||||||
eventType EventType @default(PW_Cart)
|
eventType EventType @default(PW_Cart)
|
||||||
@ -160,7 +172,7 @@ model Shift {
|
|||||||
name String
|
name String
|
||||||
startTime DateTime
|
startTime DateTime
|
||||||
endTime DateTime
|
endTime DateTime
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
requiresTransport Boolean @default(false)
|
requiresTransport Boolean @default(false)
|
||||||
notes String?
|
notes String?
|
||||||
//date DateTime
|
//date DateTime
|
||||||
@ -177,7 +189,7 @@ model Assignment {
|
|||||||
shiftId Int
|
shiftId Int
|
||||||
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
||||||
publisherId String
|
publisherId String
|
||||||
isTentative Boolean @default(false) // if no availability for it, when importing previous schedules
|
isBySystem Boolean @default(false) // if no availability for it, when importing previous schedules
|
||||||
isConfirmed Boolean @default(false)
|
isConfirmed Boolean @default(false)
|
||||||
isWithTransport Boolean @default(false)
|
isWithTransport Boolean @default(false)
|
||||||
isMailSent Boolean @default(false)
|
isMailSent Boolean @default(false)
|
||||||
@ -190,7 +202,7 @@ model Location {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
address String
|
address String
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
content String? @db.LongText
|
content String? @db.LongText
|
||||||
cartEvents CartEvent[]
|
cartEvents CartEvent[]
|
||||||
reports Report[]
|
reports Report[]
|
||||||
@ -219,7 +231,8 @@ model Report {
|
|||||||
returnVisitInfoCount Int?
|
returnVisitInfoCount Int?
|
||||||
conversationCount Int?
|
conversationCount Int?
|
||||||
|
|
||||||
experienceInfo String? @db.LongText
|
experienceInfo String? @db.LongText
|
||||||
|
type ReportType @default(ServiceReport)
|
||||||
|
|
||||||
@@map("Report")
|
@@map("Report")
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ INSERT INTO
|
|||||||
`id`,
|
`id`,
|
||||||
`name`,
|
`name`,
|
||||||
`address`,
|
`address`,
|
||||||
`isactive`
|
`isActive`
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
1,
|
1,
|
||||||
@ -57,7 +57,7 @@ INSERT INTO
|
|||||||
`startTime`,
|
`startTime`,
|
||||||
`endTime`,
|
`endTime`,
|
||||||
`dayofweek`,
|
`dayofweek`,
|
||||||
`isactive`,
|
`isActive`,
|
||||||
`locationId`,
|
`locationId`,
|
||||||
`shiftDuration`,
|
`shiftDuration`,
|
||||||
`eventType`,
|
`eventType`,
|
||||||
@ -147,7 +147,7 @@ VALUES (
|
|||||||
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */
|
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */
|
||||||
;
|
;
|
||||||
|
|
||||||
-- INSERT INTO `cartevent` (`id`, `startTime`, `endTime`, `dayofweek`, `isactive`, `locationId`, `shiftDuration`, `eventType`, `numberOfPublishers`)
|
-- INSERT INTO `cartevent` (`id`, `startTime`, `endTime`, `dayofweek`, `isActive`, `locationId`, `shiftDuration`, `eventType`, `numberOfPublishers`)
|
||||||
-- VALUES
|
-- VALUES
|
||||||
-- (2, '2023-12-27 07:00:33.174', '2023-12-27 16:00:33.174', 'Tuesday', 1, 2, 90, 'PW_Cart', 4),
|
-- (2, '2023-12-27 07:00:33.174', '2023-12-27 16:00:33.174', 'Tuesday', 1, 2, 90, 'PW_Cart', 4),
|
||||||
-- (3, '2023-12-28 07:00:33.174', '2023-12-28 16:00:33.174', 'Wednesday', 1, 3, 90, 'PW_Cart', 4),
|
-- (3, '2023-12-28 07:00:33.174', '2023-12-28 16:00:33.174', 'Wednesday', 1, 3, 90, 'PW_Cart', 4),
|
||||||
|
2
process.d.ts
vendored
2
process.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
export interface ProcessEnv {
|
export interface ProcessEnv {
|
||||||
NEXTAUTH_URL: string
|
PUBLIC_URL: string
|
||||||
NEXTAUTH_SECRET: string
|
NEXTAUTH_SECRET: string
|
||||||
GITHUB_ID: string
|
GITHUB_ID: string
|
||||||
GITHUB_SECRET: string
|
GITHUB_SECRET: string
|
||||||
|
BIN
public/content/permits/Разрешително за Март 24г.-промяна (1).pdf
Normal file
BIN
public/content/permits/Разрешително за Март 24г.-промяна (1).pdf
Normal file
Binary file not shown.
BIN
public/content/permits/Разрешително за Март 24г..pdf
Normal file
BIN
public/content/permits/Разрешително за Март 24г..pdf
Normal file
Binary file not shown.
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
89
server.js
89
server.js
@ -20,31 +20,26 @@ 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
|
console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param
|
||||||
// require('dotenv').config({ path: '.env.test' });
|
require('dotenv').config({ path: `.env.${process.env.APP_ENV}` });
|
||||||
// } else {
|
|
||||||
// // Load default environment variables
|
|
||||||
// require('dotenv').config();
|
|
||||||
// }
|
|
||||||
|
|
||||||
console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV);
|
|
||||||
require('dotenv').config({
|
|
||||||
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 PORT = process.env.NEXT_PUBLIC_PORT || 3000;
|
const PROTOCOL = process.env.PROTOCOL;
|
||||||
const HOST = process.env.NEXT_PUBLIC_HOST;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const HOST = process.env.HOST;
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
const PROTOCOL = process.env.NEXT_PUBLIC_PROTOCOL;
|
|
||||||
const nextApp = next({ dev });
|
const nextApp = next({ dev });
|
||||||
const nextHandler = nextApp.getRequestHandler();
|
const nextHandler = nextApp.getRequestHandler();
|
||||||
console.log("process.env.SSL_ENABLED = ", process.env.SSL_ENABLED);
|
console.log("process.env.PROTOCOL = ", process.env.PROTOCOL);
|
||||||
|
process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_PUBLIC_URL; //NEXTAUTH_URL mandatory for next-auth
|
||||||
|
console.log("process.env.NEXT_PUBLIC_PUBLIC_URL = ", process.env.NEXT_PUBLIC_PUBLIC_URL);
|
||||||
console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
|
console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
|
||||||
console.log("process.env.NEXT_PUBLIC_PORT = ", process.env.NEXT_PUBLIC_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');
|
||||||
|
|
||||||
@ -62,12 +57,37 @@ const uploadTmp = multer({ storage: storageMem });
|
|||||||
|
|
||||||
|
|
||||||
const prisma = common.getPrismaClient();
|
const prisma = common.getPrismaClient();
|
||||||
|
const server = express();
|
||||||
|
|
||||||
|
//check if ssl is enabled
|
||||||
|
if (process.env.PROTOCOL === 'https') {
|
||||||
|
console.log("SSL_ENABLED = true");
|
||||||
|
// Redirect from http to https
|
||||||
|
// server.use((req, res, next) => {
|
||||||
|
// if (req.headers['x-forwarded-proto'] !== 'https') {
|
||||||
|
// return res.redirect(`https://${req.headers.host}${req.url}`);
|
||||||
|
// }
|
||||||
|
// next();
|
||||||
|
// });
|
||||||
|
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
||||||
|
const options = {
|
||||||
|
key: fs.readFileSync(process.env.SSL_KEY),
|
||||||
|
cert: fs.readFileSync(process.env.SSL_CERT),
|
||||||
|
secureProtocol: 'TLSv1_2_method', // Example: Force TLS 1.2
|
||||||
|
};
|
||||||
|
https.createServer(options, server).listen(PORT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
server.listen(PORT, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log(`> Ready on ${PROTOCOL}://${HOST}:${PORT}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
// handlers
|
// handlers
|
||||||
nextApp
|
nextApp
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const server = express();
|
|
||||||
|
|
||||||
// Add the middleware to set 'x-forwarded-host' header
|
// Add the middleware to set 'x-forwarded-host' header
|
||||||
server.use((req, res, next) => {
|
server.use((req, res, next) => {
|
||||||
@ -84,6 +104,8 @@ nextApp
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico"));
|
server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico"));
|
||||||
|
// server.use("/robots.txt", express.static("styles/favicon_io/robots.txt"));
|
||||||
|
// server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
|
||||||
|
|
||||||
server.get("/last_schedule_json", (req, res) => {
|
server.get("/last_schedule_json", (req, res) => {
|
||||||
// var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
|
// var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
|
||||||
@ -259,7 +281,7 @@ nextApp
|
|||||||
|
|
||||||
var shifts = await prisma.shift.findMany({
|
var shifts = await prisma.shift.findMany({
|
||||||
where: {
|
where: {
|
||||||
isactive: true,
|
isActive: true,
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: fromDate,
|
gte: fromDate,
|
||||||
lt: toDate,
|
lt: toDate,
|
||||||
@ -410,7 +432,7 @@ nextApp
|
|||||||
|
|
||||||
var publishers = await prisma.publisher.findMany({
|
var publishers = await prisma.publisher.findMany({
|
||||||
where: {
|
where: {
|
||||||
isactive: true,
|
isActive: true,
|
||||||
email: {
|
email: {
|
||||||
not: "",
|
not: "",
|
||||||
},
|
},
|
||||||
@ -556,31 +578,6 @@ nextApp
|
|||||||
return nextHandler(req, res);
|
return nextHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
//check if ssl is enabled
|
|
||||||
if (process.env.SSL_ENABLED === "true") {
|
|
||||||
console.log("SSL_ENABLED = true");
|
|
||||||
// Redirect from http to https
|
|
||||||
// server.use((req, res, next) => {
|
|
||||||
// if (req.headers['x-forwarded-proto'] !== 'https') {
|
|
||||||
// return res.redirect(`https://${req.headers.host}${req.url}`);
|
|
||||||
// }
|
|
||||||
// next();
|
|
||||||
// });
|
|
||||||
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
|
||||||
const options = {
|
|
||||||
key: fs.readFileSync(process.env.SSL_KEY),
|
|
||||||
cert: fs.readFileSync(process.env.SSL_CERT),
|
|
||||||
secureProtocol: 'TLSv1_2_method', // Example: Force TLS 1.2
|
|
||||||
};
|
|
||||||
https.createServer(options, server).listen(PORT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
server.listen(PORT, (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
console.log(`> Ready on ${PROTOCOL}://${HOST}:${PORT}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((ex) => {
|
.catch((ex) => {
|
||||||
console.warn(`Error starting server on ${HOST}:${PORT}`)
|
console.warn(`Error starting server on ${HOST}:${PORT}`)
|
||||||
|
@ -474,7 +474,24 @@ createEvent = async (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SaveEventsInGoogleCalendar = async function SaveEventsInGoogleCalendar(events) {
|
||||||
|
// Load client secrets from a local file.
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(CREDENTIALS_PATH);
|
||||||
|
// Authorize a client with credentials, then call the Google Calendar API.
|
||||||
|
authorize(JSON.parse(content), createEvent);
|
||||||
|
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error loading client secret file:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exports.GenerateICS = GenerateICS;
|
exports.GenerateICS = GenerateICS;
|
||||||
exports.createEvent = createEvent;
|
exports.createEvent = createEvent;
|
||||||
|
exports.SaveEventsInGoogleCalendar = SaveEventsInGoogleCalendar;
|
||||||
|
|
||||||
createEvent();
|
|
||||||
|
//createEvent();
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
|
|
||||||
const levenshtein = require('fastest-levenshtein');
|
const levenshtein = require('fastest-levenshtein');
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = typeof window === 'undefined' ? require('fs') : undefined;
|
||||||
const path = require("path");
|
const path = typeof window === 'undefined' ? require('path') : undefined;
|
||||||
|
|
||||||
const { PrismaClient } = require('@prisma/client');
|
const { PrismaClient } = require('@prisma/client');
|
||||||
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
||||||
|
|
||||||
@ -75,53 +76,22 @@ exports.setBaseUrl = function (req) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.getBaseUrl = function (relative = "", req = null) {
|
exports.getBaseUrl = function (relative = "", req = null) {
|
||||||
const filePath = path.join(__dirname, 'baseUrl.txt');
|
return process.env.NEXT_PUBLIC_PUBLIC_URL + relative;
|
||||||
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
const baseUrl = fs.readFileSync(filePath, 'utf8').trim();
|
|
||||||
const fullUrl = relative ? new URL(relative, baseUrl).toString() : baseUrl;
|
|
||||||
return fullUrl;
|
|
||||||
} else {
|
|
||||||
if (req) {
|
|
||||||
const baseUrl = exports.setBaseUrl(req); // Correctly reference setBaseUrl
|
|
||||||
return `${baseUrl}/${relative.replace(/^\/|\/$/g, '')}`;
|
|
||||||
}
|
|
||||||
console.log('Base URL file does not exist.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading the base URL file:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// const host = process.env.NEXT_PUBLIC_HOST || '127.0.0.1';
|
|
||||||
// const port = process.env.NEXT_PUBLIC_PORT ? `:${ process.env.NEXT_PUBLIC_PORT } ` : '';
|
|
||||||
// const protocol = process.env.NEXT_PUBLIC_PROTOCOL || "https"
|
|
||||||
|
|
||||||
// //const url = `${ protocol }://${host}${port}/${relative.replace(/^\/|\/$/g, '')}/`;
|
|
||||||
// const isRelativeEmpty = !relative || relative.trim() === '';
|
|
||||||
// const formattedRelative = !isRelativeEmpty ? '/' + relative.replace(/^\/|\/$/g, '') : '';
|
|
||||||
// const url = `${protocol}://${host}${port}${formattedRelative}`;
|
|
||||||
|
|
||||||
|
|
||||||
// logger.debug("NODE_ENV = ", process.env.NODE_ENV, "protocol:", protocol);
|
|
||||||
// logger.debug("getBaseURL = ", url);
|
|
||||||
|
|
||||||
// return url;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 = ", process.env.DATABASE);
|
||||||
|
|
||||||
return prisma;
|
return prisma;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,24 +151,27 @@ exports.getDayOfWeekName = function (date) {
|
|||||||
return exports.dayOfWeekNames[dayOfWeekIndex];
|
return exports.dayOfWeekNames[dayOfWeekIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDayOfWeekNameEnEnum = function (date) {
|
exports.getDayOfWeekNameEnEnumForDate = function (date) {
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
const dayOfWeekIndex = date.getDayEuropean();
|
const dayOfWeekIndex = date.getDayEuropean();
|
||||||
const dayOfWeekNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
return exports.DaysOfWeekArray[dayOfWeekIndex];
|
||||||
|
|
||||||
//return enum instead of string
|
|
||||||
// const dayOfWeekNames: Record<number, DayOfWeek> = {
|
|
||||||
// 0: DayOfWeek.Monday,
|
|
||||||
// 1: DayOfWeek.Tuesday,
|
|
||||||
// 2: DayOfWeek.Wednesday,
|
|
||||||
// 3: DayOfWeek.Thursday,
|
|
||||||
// 4: DayOfWeek.Friday,
|
|
||||||
// 5: DayOfWeek.Saturday,
|
|
||||||
// 6: DayOfWeek.Sunday
|
|
||||||
// };
|
|
||||||
|
|
||||||
return dayOfWeekNames[dayOfWeekIndex];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//obsolete: we want to ensure getDayEuropean() is used, hense we will not use this function
|
||||||
|
// exports.getDayOfWeekNameEnEnum = function (dayOfWeekIndex) {
|
||||||
|
// //return enum instead of string
|
||||||
|
// // const dayOfWeekNames: Record<number, DayOfWeek> = {
|
||||||
|
// // 0: DayOfWeek.Monday,
|
||||||
|
// // 1: DayOfWeek.Tuesday,
|
||||||
|
// // 2: DayOfWeek.Wednesday,
|
||||||
|
// // 3: DayOfWeek.Thursday,
|
||||||
|
// // 4: DayOfWeek.Friday,
|
||||||
|
// // 5: DayOfWeek.Saturday,
|
||||||
|
// // 6: DayOfWeek.Sunday
|
||||||
|
// // };
|
||||||
|
// return exports.DaysOfWeekArray[dayOfWeekIndex];
|
||||||
|
|
||||||
|
// }
|
||||||
exports.getPubTypeEnum = function (text) {
|
exports.getPubTypeEnum = function (text) {
|
||||||
const input = text.trim();
|
const input = text.trim();
|
||||||
const mapping = {
|
const mapping = {
|
||||||
@ -228,15 +201,52 @@ exports.getDayOfWeekDate = function (dayOfWeekName, date = new Date()) {
|
|||||||
return date;
|
return date;
|
||||||
};
|
};
|
||||||
//common.getWeekOfMonth(date)
|
//common.getWeekOfMonth(date)
|
||||||
// exports.getWeekOfMonth = function (date) {
|
exports.getWeekOfMonth = function (inputDate) {
|
||||||
// // Copy date so don't modify original
|
let date = new Date(inputDate);
|
||||||
// date = new Date(date);
|
let firstDayOfMonth = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||||
// // Adjust to Monday of this week
|
let firstMonday = new Date(firstDayOfMonth);
|
||||||
// date.setDate(date.getDate() + 3 - (date.getDayEuropean() + 6) % 7);
|
|
||||||
// // Return week number
|
// Adjust firstDayOfMonth to the first Monday of the month
|
||||||
// const weekNumber = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 / 7);
|
if (firstDayOfMonth.getDay() === 0) { // Sunday
|
||||||
// return weekNumber;
|
firstMonday.setDate(2);
|
||||||
// }
|
} else if (firstDayOfMonth.getDay() !== 1) { // Not Monday
|
||||||
|
firstMonday.setDate(9 - firstDayOfMonth.getDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the difference in days
|
||||||
|
let diff = (date - firstMonday) / (1000 * 60 * 60 * 24);
|
||||||
|
// Calculate week number
|
||||||
|
let weekNumber = Math.ceil((diff + 1) / 7);
|
||||||
|
|
||||||
|
return weekNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getDateFromWeekNrAndDayOfWeek = function (firstMonday, weekNr, dayOfWeekEnum, startTime) {
|
||||||
|
firstMonday = new Date(firstMonday);
|
||||||
|
startTime = new Date(startTime);
|
||||||
|
if (!weekNr || weekNr < 1 || weekNr > 5) {
|
||||||
|
weekNr = this.getWeekOfMonth(startTime);
|
||||||
|
}
|
||||||
|
//get int from dayOfWeekEnum
|
||||||
|
let dayOfWeekNr = this.getDayOfWeekIndex(dayOfWeekEnum);
|
||||||
|
if (dayOfWeekNr < 0 || dayOfWeekNr > 6) {
|
||||||
|
dayOfWeekNr = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the day offset from the first Monday of the month
|
||||||
|
// Note: Assuming dayOfWeekEnum starts from 0 (Monday) to 6 (Sunday)
|
||||||
|
const daysFromFirstMonday = (weekNr - 1) * 7 + dayOfWeekNr;
|
||||||
|
|
||||||
|
// Calculate the new date
|
||||||
|
let newStart = new Date(firstMonday);
|
||||||
|
newStart.setDate(firstMonday.getDate() + daysFromFirstMonday);
|
||||||
|
|
||||||
|
// Extract time from startTime and apply it to newStart
|
||||||
|
const time = new Date(startTime);
|
||||||
|
newStart.setHours(time.getHours(), time.getMinutes(), time.getSeconds());
|
||||||
|
|
||||||
|
return newStart;
|
||||||
|
}
|
||||||
|
|
||||||
exports.getMonthDatesInfo = function (date) {
|
exports.getMonthDatesInfo = function (date) {
|
||||||
// get first day of the month
|
// get first day of the month
|
||||||
@ -275,6 +285,9 @@ exports.getMonthDatesInfo = function (date) {
|
|||||||
// lastSunday.setDate(firstDayNextMonth.getDate() - firstDayNextMonth.getDay());
|
// lastSunday.setDate(firstDayNextMonth.getDate() - firstDayNextMonth.getDay());
|
||||||
|
|
||||||
//logger.debug("Last Sunday: ", lastSunday);
|
//logger.debug("Last Sunday: ", lastSunday);
|
||||||
|
const diffInDays = (lastSunday - firstMonday) / (1000 * 60 * 60 * 24);
|
||||||
|
// Calculate number of weeks, rounding up for partial weeks
|
||||||
|
const nrOfWeeks = Math.ceil((diffInDays + 1) / 7);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
firstDay: firstDay,
|
firstDay: firstDay,
|
||||||
@ -285,9 +298,10 @@ exports.getMonthDatesInfo = function (date) {
|
|||||||
date: date,
|
date: date,
|
||||||
monthName: monthName,
|
monthName: monthName,
|
||||||
year: date.getFullYear(),
|
year: date.getFullYear(),
|
||||||
nrOfWeeks: Math.ceil((lastMonday.getDate() - firstMonday.getDate()) / 7)
|
nrOfWeeks: nrOfWeeks
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
exports.getMonthInfo = exports.getMonthDatesInfo;
|
||||||
|
|
||||||
exports.getMonthlyScheduleRange = function (date) {
|
exports.getMonthlyScheduleRange = function (date) {
|
||||||
let info = exports.getMonthDatesInfo(date);
|
let info = exports.getMonthDatesInfo(date);
|
||||||
@ -511,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
|
||||||
@ -715,3 +731,7 @@ exports.getLocalStorage = function (key, defaultValue) {
|
|||||||
}
|
}
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.root = function (req) {
|
||||||
|
return process.env.NEXT_PUBLIC_PUBLIC_URL;
|
||||||
|
}
|
||||||
|
@ -79,10 +79,7 @@ async function findPublisher(names, email, select, getAll = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function findPublisherAvailability(publisherId, date) {
|
async function findPublisherAvailability(publisherId, date) {
|
||||||
|
|
||||||
const prisma = common.getPrismaClient();
|
const prisma = common.getPrismaClient();
|
||||||
const dayOfWeek = common.getDayOfWeekNameEnEnum(date); // Assuming common.getDayOfWeekNameEnEnum returns the day of week
|
|
||||||
//const weekOfMonth = common.getWeekOfMonth(date); // Assuming common.getWeekOfMonth returns the week of month
|
|
||||||
date = new Date(date); // Convert to date object if not already
|
date = new Date(date); // Convert to date object if not already
|
||||||
const hours = date.getHours();
|
const hours = date.getHours();
|
||||||
const minutes = date.getMinutes();
|
const minutes = date.getMinutes();
|
||||||
@ -90,32 +87,24 @@ async function findPublisherAvailability(publisherId, date) {
|
|||||||
const potentialAvailabilities = await prisma.availability.findMany({
|
const potentialAvailabilities = await prisma.availability.findMany({
|
||||||
where: {
|
where: {
|
||||||
publisherId: publisherId,
|
publisherId: publisherId,
|
||||||
OR: [
|
AND: [ // Ensure both conditions must be met
|
||||||
{
|
{
|
||||||
// Exact date match
|
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: new Date(date.setHours(0, 0, 0, 0)),
|
lte: new Date(date), // startTime is less than or equal to the date
|
||||||
lt: new Date(date.setHours(23, 59, 59, 999))
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Correct day of week and before the date, with endDate consideration
|
endTime: {
|
||||||
dayofweek: dayOfWeek,
|
gte: new Date(date), // endTime is greater than or equal to the date
|
||||||
OR: [
|
},
|
||||||
{
|
},
|
||||||
endDate: null
|
],
|
||||||
},
|
|
||||||
{
|
|
||||||
endDate: {
|
|
||||||
gt: date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (potentialAvailabilities.length === 0) {
|
||||||
|
return null; // No availability found
|
||||||
|
}
|
||||||
// Filter the results based on time and other criteria when not exact date match
|
// Filter the results based on time and other criteria when not exact date match
|
||||||
const availability = potentialAvailabilities.find(avail => {
|
const availability = potentialAvailabilities.find(avail => {
|
||||||
const availStartHours = avail.startTime.getHours();
|
const availStartHours = avail.startTime.getHours();
|
||||||
@ -147,7 +136,7 @@ async function getAvailabilities(userId) {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
isactive: true,
|
isActive: true,
|
||||||
isFromPreviousAssignment: true,
|
isFromPreviousAssignment: true,
|
||||||
dayofweek: true,
|
dayofweek: true,
|
||||||
dayOfMonth: true,
|
dayOfMonth: true,
|
||||||
@ -182,7 +171,7 @@ async function getAvailabilities(userId) {
|
|||||||
shiftId Int
|
shiftId Int
|
||||||
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
||||||
publisherId String
|
publisherId String
|
||||||
isactive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isConfirmed Boolean @default(false)
|
isConfirmed Boolean @default(false)
|
||||||
isWithTransport Boolean @default(false)
|
isWithTransport Boolean @default(false)
|
||||||
Report Report[]
|
Report Report[]
|
||||||
@ -194,7 +183,7 @@ async function getAvailabilities(userId) {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
isTentative: true,
|
isBySystem: true,
|
||||||
isConfirmed: true,
|
isConfirmed: true,
|
||||||
isWithTransport: true,
|
isWithTransport: true,
|
||||||
shift: {
|
shift: {
|
||||||
|
@ -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);
|
|
||||||
};
|
|
||||||
|
@ -323,21 +323,21 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
}
|
}
|
||||||
var shifts = await prisma.shift.findMany({
|
var shifts = await prisma.shift.findMany({
|
||||||
where: {
|
where: {
|
||||||
isactive: true,
|
isActive: true,
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: monthDatesInfo.firstMonday,
|
gte: monthDatesInfo.firstMonday,
|
||||||
lt: monthDatesInfo.lastSunday,
|
lt: monthDatesInfo.lastSunday,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var locations = await prisma.location.findMany({ where: { isactive: true, } });
|
var locations = await prisma.location.findMany({ where: { isActive: true, } });
|
||||||
|
|
||||||
var cartEvents = await prisma.cartEvent.findMany({ where: { isactive: true, } });
|
var cartEvents = await prisma.cartEvent.findMany({ where: { isActive: true, } });
|
||||||
|
|
||||||
var publishers = await prisma.publisher.findMany({
|
var publishers = await prisma.publisher.findMany({
|
||||||
where: { isactive: true, },
|
where: { isActive: true, },
|
||||||
include: {
|
include: {
|
||||||
availabilities: { where: { isactive: true, }, },
|
availabilities: { where: { isActive: true, }, },
|
||||||
assignments: { include: { shift: true, }, },
|
assignments: { include: { shift: true, }, },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -388,7 +388,7 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var dayofWeek = common.getDayOfWeekNameEnEnum(date);
|
var dayofWeek = common.getDayOfWeekNameEnEnumForDate(date);
|
||||||
const cartEvent = cartEvents.find(
|
const cartEvent = cartEvents.find(
|
||||||
(ce) =>
|
(ce) =>
|
||||||
ce.locationId === location.id &&
|
ce.locationId === location.id &&
|
||||||
@ -404,20 +404,21 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
s.cartEventId === cartEvent.id &&
|
s.cartEventId === cartEvent.id &&
|
||||||
new Date(s.startTime).getTime() === new Date(start).getTime()
|
new Date(s.startTime).getTime() === new Date(start).getTime()
|
||||||
);
|
);
|
||||||
|
// get only hh:mm from the date
|
||||||
|
let isTransportRequired = event.shiftNr == 1 || end.toLocaleTimeString().substring(0, 5) == cartEvent.endTime.toLocaleTimeString().substring(0, 5);
|
||||||
if (!shift) {
|
if (!shift) {
|
||||||
//if shiftnr = 1, notes = "Докарва" + event.transport
|
//if shiftnr = 1, notes = "Докарва" + event.transport
|
||||||
//if shiftnr = 8, notes = "Взема" + event.transport
|
//if shiftnr = 8, notes = "Взема" + event.transport
|
||||||
|
|
||||||
|
let note = isTransportRequired ? event.transport : "";
|
||||||
let note = event.shiftNr === 1 ? "Докарва количка от Люлин - " + event.transport :
|
// "Докарва количка от Люлин/Прибира количка в Люлин"
|
||||||
event.shiftNr === 6 ? "Прибира количка в Люлин - " + event.transport : "";
|
|
||||||
const shiftEntity = await prisma.shift.create({
|
const shiftEntity = await prisma.shift.create({
|
||||||
data: {
|
data: {
|
||||||
name: event.dayOfWeek + " " + event.dayOfMonth + ", " + start.toLocaleTimeString() + " - " + end.toLocaleTimeString(),
|
name: event.dayOfWeek + " " + event.dayOfMonth + ", " + start.toLocaleTimeString() + " - " + end.toLocaleTimeString(),
|
||||||
startTime: start,
|
startTime: start,
|
||||||
endTime: end,
|
endTime: end,
|
||||||
notes: note,
|
notes: note,
|
||||||
|
requiresTransport: isTransportRequired,
|
||||||
cartEvent: {
|
cartEvent: {
|
||||||
connect: {
|
connect: {
|
||||||
id: cartEvent.id,
|
id: cartEvent.id,
|
||||||
@ -471,7 +472,7 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
email: name.toLowerCase().replace(/ /g, "."), // + "@gmail.com"
|
email: name.toLowerCase().replace(/ /g, "."), // + "@gmail.com"
|
||||||
firstName: firstname,
|
firstName: firstname,
|
||||||
lastName: lastname,
|
lastName: lastname,
|
||||||
isactive: true,
|
isActive: true,
|
||||||
isImported: true,
|
isImported: true,
|
||||||
// role: "EXTERNAL",
|
// role: "EXTERNAL",
|
||||||
};
|
};
|
||||||
@ -481,22 +482,22 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
// create availability with the same date as the event.
|
// create availability with the same date as the event.
|
||||||
//ToDo: add parameter to control if we want to create availability for each event. can be done whe we import previous shifts.
|
//ToDo: add parameter to control if we want to create availability for each event. can be done whe we import previous shifts.
|
||||||
// if (createAvailabilities) {
|
// if (createAvailabilities) {
|
||||||
// const dayofWeek = common.getDayOfWeekNameEnEnum(date);
|
// const dayofWeek = common.getDayOfWeekNameEnEnumForDate(date);
|
||||||
// const availability = await prisma.availability.create({
|
// const availability = await prisma.availability.create({
|
||||||
// data: {
|
// data: {
|
||||||
// publisherId: publisher.id,
|
// publisherId: publisher.id,
|
||||||
// //date: date,
|
|
||||||
// dayofweek: dayofWeek,
|
// dayofweek: dayofWeek,
|
||||||
// startTime: startTime,
|
// startTime: startTime,
|
||||||
// endTime: endTime,
|
// endTime: endTime,
|
||||||
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`,
|
// name: `от график, ${publisher.firstName} ${publisher.lastName}`,
|
||||||
// isFromPreviousAssignment: true,
|
// isFromPreviousAssignment: true,
|
||||||
// isactive: true,
|
// isActive: true,
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
|
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
|
||||||
// }
|
// }
|
||||||
// const personResponse = await axiosInstance.post("/publishers", manualPub);
|
|
||||||
|
const personResponse = await axiosInstance.post("/publishers", manualPub);
|
||||||
// let personId = personResponse.data.id;
|
// let personId = personResponse.data.id;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -506,6 +507,13 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (location != null && publisher != null && shift != null) {
|
if (location != null && publisher != null && shift != null) {
|
||||||
|
let isWithTransport = false;
|
||||||
|
if (isTransportRequired) {
|
||||||
|
const pubInitials = publisher.firstName[0] + publisher.lastName[0];
|
||||||
|
// get cotent after last - or long dash-`-` and remove spaces, trim dots and make lowercase
|
||||||
|
let transportInitials = event.transport.split("-").pop().replace(/[\s.]/g, "").toUpperCase();
|
||||||
|
isWithTransport = transportInitials.includes(pubInitials);
|
||||||
|
}
|
||||||
const assignment = await prisma.assignment.create({
|
const assignment = await prisma.assignment.create({
|
||||||
data: {
|
data: {
|
||||||
//publisherId: publisher.id,
|
//publisherId: publisher.id,
|
||||||
@ -520,13 +528,15 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
id: shift.id,
|
id: shift.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isWithTransport: isWithTransport,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//ToDo: fix findPublisherAvailability and creation of availabilities
|
//ToDo: fix findPublisherAvailability and creation of availabilities
|
||||||
// check if there is an availability for this publisher on this date, and if not, create one
|
// check if there is an availability for this publisher on this date, and if not, create one
|
||||||
|
//ToDo: check if that works
|
||||||
// const availability = await data.findPublisherAvailability(publisher.id, start);
|
// const availability = await data.findPublisherAvailability(publisher.id, start);
|
||||||
// if (!availability && createAvailabilities) {
|
// if (!availability && createAvailabilities) {
|
||||||
// const dayofWeek = common.getDayOfWeekNameEnEnum(date);
|
// const dayofWeek = common.getDayOfWeekNameEnEnumForDate(date);
|
||||||
// const availability = await prisma.availability.create({
|
// const availability = await prisma.availability.create({
|
||||||
// data: {
|
// data: {
|
||||||
// publisherId: publisher.id,
|
// publisherId: publisher.id,
|
||||||
@ -537,9 +547,11 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
|
|||||||
// endTime: end,
|
// endTime: end,
|
||||||
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`,
|
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`,
|
||||||
// isFromPreviousAssignment: true,
|
// isFromPreviousAssignment: true,
|
||||||
|
// isWithTransportIn: isWithTransport && event.shiftNr == 1,
|
||||||
|
// isWithTransportOut: isWithTransport && event.shiftNr > 1,
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
|
// console.log(`Created SYSTEM availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
console.log(`Created assignment with ID ${assignment.id} for date '${date.toDateString()}' and location '${event.placeOfEvent}'. publisher: ${publisher.firstName} ${publisher.lastName}}`);
|
console.log(`Created assignment with ID ${assignment.id} for date '${date.toDateString()}' and location '${event.placeOfEvent}'. publisher: ${publisher.firstName} ${publisher.lastName}}`);
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
//??? can we consolidate all imports into one file?
|
|
||||||
import ProtectedRoute from '../../../components/protectedRoute';
|
|
||||||
import axiosInstance from '../../../src/axiosSecure';
|
|
||||||
import Layout from "../../../components/layout";
|
|
@ -11,7 +11,7 @@ SELECT DISTINCT Publisher.*
|
|||||||
FROM Publisher
|
FROM Publisher
|
||||||
INNER JOIN Availability ON Availability.publisherId = Publisher.id
|
INNER JOIN Availability ON Availability.publisherId = Publisher.id
|
||||||
WHERE
|
WHERE
|
||||||
Availability.isactive = true
|
Availability.isActive = true
|
||||||
AND ( (
|
AND ( (
|
||||||
Availability.dayOfMonth IS NOT NULL
|
Availability.dayOfMonth IS NOT NULL
|
||||||
AND Availability.startTime <= '2023-03-30 13:00:00'
|
AND Availability.startTime <= '2023-03-30 13:00:00'
|
||||||
@ -30,7 +30,7 @@ SELECT DISTINCT Publisher.*
|
|||||||
FROM Publisher
|
FROM Publisher
|
||||||
INNER JOIN Availability ON Availability.publisherId = Publisher.id
|
INNER JOIN Availability ON Availability.publisherId = Publisher.id
|
||||||
WHERE
|
WHERE
|
||||||
Availability.isactive = true
|
Availability.isActive = true
|
||||||
AND (Availability.dayOfMonth = 5) clfuyo33e005aknvchf1wm3bu All publishers: 121;
|
AND (Availability.dayOfMonth = 5) clfuyo33e005aknvchf1wm3bu All publishers: 121;
|
||||||
|
|
||||||
(118) unique,
|
(118) unique,
|
||||||
|
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>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user