Merge branch 'main' into feature-fixStats
22
.env
@ -2,7 +2,7 @@
|
|||||||
# HOST=localhost
|
# HOST=localhost
|
||||||
# PORT=3003
|
# PORT=3003
|
||||||
# NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003
|
# NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003
|
||||||
|
ENV_ENV='.env'
|
||||||
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
||||||
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
||||||
|
|
||||||
@ -10,12 +10,20 @@ NODE_ENV=development
|
|||||||
# mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma
|
# mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma
|
||||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||||
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||||
|
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
||||||
|
|
||||||
# // 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
|
||||||
|
# callback https://sofia.mwitnessing.com/api/auth/callback/google
|
||||||
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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
|
||||||
|
|
||||||
|
|
||||||
|
# //https://sofia.mwitnessing.com/api/auth/callback/microsoft
|
||||||
|
# https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
|
||||||
|
# owner: dobromirpopovgateway.onmicrosoft.com dobromir.popov@gateway.one (personal) Doby Popov P One
|
||||||
|
# callback https://sofia.mwhitnessing.com/api/auth/callback/azure-ad
|
||||||
|
|
||||||
AZURE_AD_CLIENT_ID=9e13bedd-1f9d-4c23-910e-a806aba308b6 # Application (client) ID
|
AZURE_AD_CLIENT_ID=9e13bedd-1f9d-4c23-910e-a806aba308b6 # Application (client) ID
|
||||||
AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret value
|
AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret value
|
||||||
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
||||||
@ -24,6 +32,9 @@ AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
|||||||
|
|
||||||
APPLE_TEAM_ID=XC57P9SXDK
|
APPLE_TEAM_ID=XC57P9SXDK
|
||||||
APPLE_KEY_ID=TB3V355G5Y
|
APPLE_KEY_ID=TB3V355G5Y
|
||||||
|
APPLE_APP_ID=com.mwhitnessing.sofia
|
||||||
|
APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IjlRVzkyNkZTSzkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxNDY3MDQxOSwiZXhwIjoxNzMwMjIyNDIwLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.KUW2roM2MAyfe2RphAoeAB-OK4LolGcO347SCxIocM3RXR0Z_5GVwu0BJiHwh2nO4WGXi2xHJgBvuwZhdAPWug
|
||||||
|
APPLE_PK=-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgLJtuaml9xsCzcKSH\nRvaTqmxoQgPzxXtk9jWZGU90FQCgCgYIKoZIzj0DAQehRANCAATM910/AhLshLvn\nWbmWi7F580AqLoNvHKHB4A1bccz+9QSvj0AcYA4J0BiMFfQrhXC5/SKEe7I0pDcv\nn4UlL3Sx\n-----END PRIVATE KEY-----
|
||||||
|
|
||||||
# APPLE_APP_ID=com.mwhitnessing.sofia
|
# APPLE_APP_ID=com.mwhitnessing.sofia
|
||||||
# APPLE_SECRET=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJpYXQiOjE3MTMzMDQ1OTMsImV4cCI6MTcyODg1NjU5MywiYXVkIjoiaHR0cHM6Ly9hcHBsZWlkLmFwcGxlLmNvbSIsImlzcyI6IlhDNTdQOVNYREsiLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.iO2prjQ_4P7F17R7LTJfG9zHluj59uUtm8DA1LbK49jVBMeGHQP_Az7s_yU5D-GeMHSwU7VnVHcaVKiGWT_Yjg
|
# APPLE_SECRET=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJpYXQiOjE3MTMzMDQ1OTMsImV4cCI6MTcyODg1NjU5MywiYXVkIjoiaHR0cHM6Ly9hcHBsZWlkLmFwcGxlLmNvbSIsImlzcyI6IlhDNTdQOVNYREsiLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.iO2prjQ_4P7F17R7LTJfG9zHluj59uUtm8DA1LbK49jVBMeGHQP_Az7s_yU5D-GeMHSwU7VnVHcaVKiGWT_Yjg
|
||||||
@ -32,10 +43,6 @@ APPLE_KEY_ID=TB3V355G5Y
|
|||||||
#APPLE_APP_ID=XC57P9SXDK.com.mwhitnessing.sofia
|
#APPLE_APP_ID=XC57P9SXDK.com.mwhitnessing.sofia
|
||||||
#APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
#APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
||||||
# to generate
|
# to generate
|
||||||
APPLE_APP_ID=com.mwhitnessing.sofia
|
|
||||||
APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IjlRVzkyNkZTSzkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxNDY3MDQxOSwiZXhwIjoxNzMwMjIyNDIwLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.KUW2roM2MAyfe2RphAoeAB-OK4LolGcO347SCxIocM3RXR0Z_5GVwu0BJiHwh2nO4WGXi2xHJgBvuwZhdAPWug
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
||||||
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
||||||
@ -67,5 +74,6 @@ MAILTRAP_PASS=c7bc05f171c96c
|
|||||||
TELEGRAM_BOT=false
|
TELEGRAM_BOT=false
|
||||||
TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM
|
TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM
|
||||||
|
|
||||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY=BGxXJ0jdsQ4ihE7zp8mxrBO-QPSjeEtO9aCtPoMTuxc1VLW0OfRIt-DYinK9ekjTl2w-j0eQbeprIyBCpmmfciI
|
WEB_PUSH_EMAIL=mwitnessing@gmail.com
|
||||||
VAPID_PRIVATE_KEY=VXHu2NgcyM4J4w3O4grkS_0yLwWHCvVKDJexyBjqgx0
|
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=BGxXJ0jdsQ4ihE7zp8mxrBO-QPSjeEtO9aCtPoMTuxc1VLW0OfRIt-DYinK9ekjTl2w-j0eQbeprIyBCpmmfciI
|
||||||
|
WEB_PUSH_PRIVATE_KEY=VXHu2NgcyM4J4w3O4grkS_0yLwWHCvVKDJexyBjqgx0
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
|
ENV_ENV=.env.development
|
||||||
PROTOCOL=https
|
PROTOCOL=https
|
||||||
PORT=3003
|
PORT=3003
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
|
13
.env.development.devserver
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# .ENV for vscode server .11 dev server #
|
||||||
|
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
PROTOCOL=http
|
||||||
|
PORT=3003
|
||||||
|
HOST=cart.d-popov.com
|
||||||
|
NEXT_PUBLIC_PUBLIC_URL=https://cart.d-popov.com
|
||||||
|
DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||||
|
|
||||||
|
EMAIL_SENDER='"ССОМ [ТЕСТ] " <mwitnessing@gmail.com>'
|
@ -1,3 +1,5 @@
|
|||||||
|
# .ENV for vscode server .11 dev server #
|
||||||
|
|
||||||
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
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
19
.env.test
@ -1,4 +1,6 @@
|
|||||||
|
# trying to run .env.test.staging did not work... falling back to .env.test
|
||||||
NODE_ENV=test
|
NODE_ENV=test
|
||||||
|
ENV_ENV=test
|
||||||
|
|
||||||
PROTOCOL=http
|
PROTOCOL=http
|
||||||
HOST=staging.mwitnessing.com
|
HOST=staging.mwitnessing.com
|
||||||
@ -6,19 +8,4 @@ PORT=
|
|||||||
NEXT_PUBLIC_PUBLIC_URL=https://staging.mwitnessing.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=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
|
||||||
DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
|
|
||||||
|
|
||||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
|
||||||
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
|
||||||
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
|
|
||||||
|
|
||||||
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
|
||||||
# GOOGLE_SECRET=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
|
|
||||||
|
|
||||||
# EMAIL_SERVICE=mailtrap
|
|
||||||
# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
|
||||||
# MAILTRAP_HOST=live.smtp.mailtrap.io
|
|
||||||
# MAILTRAP_USER=api
|
|
||||||
# MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
|
26
.env.test.staging
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# trying to run .env.test.staging did not work... falling back to .env.test
|
||||||
|
NODE_ENV=test
|
||||||
|
ENV_ENV=test.staging
|
||||||
|
|
||||||
|
PROTOCOL=http
|
||||||
|
HOST=staging.mwitnessing.com
|
||||||
|
PORT=
|
||||||
|
NEXT_PUBLIC_PUBLIC_URL=https://staging.mwitnessing.com
|
||||||
|
|
||||||
|
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
||||||
|
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||||
|
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||||
|
DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
|
||||||
|
|
||||||
|
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
||||||
|
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
||||||
|
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
|
||||||
|
|
||||||
|
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
||||||
|
# GOOGLE_SECRET=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
|
||||||
|
|
||||||
|
# EMAIL_SERVICE=mailtrap
|
||||||
|
# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||||
|
# MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||||
|
# MAILTRAP_USER=api
|
||||||
|
# MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
2
.gitignore
vendored
@ -33,6 +33,6 @@ next-cart-app.zip
|
|||||||
!public/uploads/thumb/
|
!public/uploads/thumb/
|
||||||
certificates
|
certificates
|
||||||
content/output/*
|
content/output/*
|
||||||
baseUrl.txt
|
|
||||||
public/content/output/*
|
public/content/output/*
|
||||||
public/content/output/shifts 2024.1.json
|
public/content/output/shifts 2024.1.json
|
||||||
|
!public/content/uploads/*
|
||||||
|
12
.vscode/launch.json
vendored
@ -41,7 +41,7 @@
|
|||||||
"type": "node-terminal"
|
"type": "node-terminal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Run conda nodemon (DEV)",
|
"name": "Conda debug (DB)",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
@ -50,6 +50,16 @@
|
|||||||
"APP_ENV": "development.popov"
|
"APP_ENV": "development.popov"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Conda run (DB)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"command": "conda activate node && npm install && npm run start-env",
|
||||||
|
"env": {
|
||||||
|
"APP_ENV": "development.devserver"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Run conda npm TEST",
|
"name": "Run conda npm TEST",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
nextjs-app: # https://sofia.mwitnessing.com/
|
nextjs-app: # https://sofia.mwhitnessing.com/
|
||||||
hostname: jwpw-app-staging # jwpw-nextjs-app-1
|
hostname: jwpw-app-staging # jwpw-nextjs-app-1
|
||||||
image: docker.d-popov.com/jwpw:latest
|
image: docker.d-popov.com/jwpw:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/docker_volumes/pw-demo/app/public/content/uploads/:/app/public/content/uploads
|
- /mnt/docker_volumes/pw-demo/app/public/content/uploads/:/app/public/content/uploads
|
||||||
|
- /mnt/docker_volumes/pw-demo/app/logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- APP_ENV=test
|
- APP_ENV=test
|
||||||
- NODE_ENV=test
|
- NODE_ENV=test
|
||||||
@ -14,12 +15,13 @@ services:
|
|||||||
- GIT_BRANCH=main
|
- GIT_BRANCH=main
|
||||||
- GIT_USERNAME=deploy
|
- GIT_USERNAME=deploy
|
||||||
- GIT_PASSWORD=L3Kr2R438u4F7
|
- GIT_PASSWORD=L3Kr2R438u4F7
|
||||||
command: sh -c " cd /app && npm install && npx next build && npm run nodeenv; tail -f /dev/null"
|
command: sh -c " cd /app && npm install && npx next build && npm run start-env; tail -f /dev/null"
|
||||||
tty: true
|
tty: true
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- infrastructure_default
|
- infrastructure_default
|
||||||
|
- default
|
||||||
mariadb:
|
mariadb:
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
@ -33,6 +35,11 @@ services:
|
|||||||
MYSQL_DATABASE: jwpwsofia_demo
|
MYSQL_DATABASE: jwpwsofia_demo
|
||||||
MYSQL_USER: jwpwsofia_demo
|
MYSQL_USER: jwpwsofia_demo
|
||||||
MYSQL_PASSWORD: dwxhns9p9vp248
|
MYSQL_PASSWORD: dwxhns9p9vp248
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 5002:8080
|
||||||
networks:
|
networks:
|
||||||
infrastructure_default:
|
infrastructure_default:
|
||||||
external: true
|
external: true
|
11
_deploy/maintenance/default.conf
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
35
_deploy/maintenance/index.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="bg">
|
||||||
|
<!-- put that file in the web root of the maintenance container and rename it. We use nginx, so the path is /usr/share/nginx/html/index.html -->
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Поддръжка на сайта</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Поддръжка и обновления!</h1>
|
||||||
|
<p>Съжаляваме за неудобството, но в момента извършваме поддръжка на сайта. Ще се върнем онлайн скоро!</p>
|
||||||
|
<p>— Екипът на ССОМ: Специално Свидетелстване на Обществени Места - София</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -5,8 +5,9 @@ services:
|
|||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/docker_volumes/maintenance:/usr/share/nginx/html
|
- /mnt/docker_volumes/maintenance:/usr/share/nginx/html
|
||||||
|
- /mnt/docker_volumes/maintenance/default.conf:/etc/nginx/conf.d/default.conf
|
||||||
ports:
|
ports:
|
||||||
- "81:80"
|
- "3010:80"
|
||||||
environment:
|
environment:
|
||||||
- NGINX_HOST=nginx
|
- NGINX_HOST=nginx
|
||||||
- NGINX_PORT=80
|
- NGINX_PORT=80
|
@ -135,6 +135,12 @@ npx prisma migrate resolve --applied 20240201214719_assignment_add_repeat_freque
|
|||||||
npx prisma migrate dev --schema "mysql://cart:cart2023@192.168.0.10:3306/cart_dev" # -- does not work
|
npx prisma migrate dev --schema "mysql://cart:cart2023@192.168.0.10:3306/cart_dev" # -- does not work
|
||||||
|
|
||||||
|
|
||||||
|
## ---------------------- import database --------------------------------- ##
|
||||||
|
gunzip < /prisma/backups/jwpwsofia-20240430-bak.gz | mysql -u mysql_username -p database_name
|
||||||
|
|
||||||
|
#export
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,36 +1,62 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import common from '../src/helpers/common'; // Ensure this path is correct
|
import common from '../src/helpers/common'; // Ensure this path is correct
|
||||||
|
//use session to get user role
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
import e from 'express';
|
||||||
|
import ProtectedRoute from './protectedRoute';
|
||||||
|
import { UserRole } from '@prisma/client';
|
||||||
|
|
||||||
function PwaManager() {
|
function PwaManager({ subs }) {
|
||||||
|
//ToDo: for iOS, try to use apn? https://github.com/node-apn/node-apn/blob/master/doc/apn.markdown
|
||||||
|
const isSupported = () =>
|
||||||
|
'Notification' in window &&
|
||||||
|
'serviceWorker' in navigator &&
|
||||||
|
'PushManager' in window
|
||||||
|
|
||||||
|
const [inProgress, setInProgress] = useState(false)
|
||||||
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
||||||
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
||||||
const [isStandAlone, setIsStandAlone] = useState(false);
|
const [isStandAlone, setIsStandAlone] = useState(false);
|
||||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||||
const [subscription, setSubscription] = useState(null);
|
const [subscription, setSubscription] = useState(null);
|
||||||
const [registration, setRegistration] = useState(null);
|
const [registration, setRegistration] = useState(null);
|
||||||
const [notificationPermission, setNotificationPermission] = useState(Notification.permission);
|
const [notificationPermission, setNotificationPermission] = useState(isSupported() && Notification.permission);
|
||||||
|
const [_subs, setSubs] = useState(subs)
|
||||||
|
|
||||||
|
const { data: session } = useSession();
|
||||||
|
// let isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
||||||
|
let isAdmin = false;
|
||||||
|
if (session) {
|
||||||
|
isAdmin = session.user.role === UserRole.ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handle PWA installation
|
// Handle PWA installation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isSupported()) {
|
||||||
setNotificationPermission(Notification.permission);
|
setNotificationPermission(Notification.permission);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Push Notification Subscription
|
// Handle Push Notification Subscription
|
||||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||||
navigator.serviceWorker.ready.then(reg => {
|
navigator.serviceWorker.ready.then(swreg => {
|
||||||
reg.pushManager.getSubscription().then(sub => {
|
swreg.pushManager.getSubscription().then(sub => {
|
||||||
if (sub) {
|
if (sub) {
|
||||||
setSubscription(sub);
|
setSubscription(sub);
|
||||||
setIsSubscribed(true);
|
setIsSubscribed(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setRegistration(reg);
|
setRegistration(swreg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if the app is running in standalone mode
|
// Check if the app is running in standalone mode
|
||||||
|
// const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
// if (isStandalone) {
|
||||||
|
// console.log('Running in standalone mode');
|
||||||
|
// setIsPWAInstalled(true);
|
||||||
|
// }
|
||||||
|
|
||||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||||
setIsStandAlone(true);
|
setIsStandAlone(true);
|
||||||
}
|
}
|
||||||
@ -53,48 +79,80 @@ function PwaManager() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const installPWA = async (e) => {
|
|
||||||
|
|
||||||
e.preventDefault();
|
const installPWA = async (e) => {
|
||||||
|
console.log('Attempting to install PWA');
|
||||||
|
e.preventDefault(); // Prevent default button action
|
||||||
if (deferredPrompt) {
|
if (deferredPrompt) {
|
||||||
|
console.log('Prompting install');
|
||||||
deferredPrompt.prompt();
|
deferredPrompt.prompt();
|
||||||
const { outcome } = await deferredPrompt.userChoice;
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
console.log('Installation outcome:', outcome);
|
||||||
if (outcome === 'accepted') {
|
if (outcome === 'accepted') {
|
||||||
|
console.log('User accepted the A2HS prompt');
|
||||||
setIsPWAInstalled(true);
|
setIsPWAInstalled(true);
|
||||||
|
} else {
|
||||||
|
console.log('User dismissed the A2HS prompt');
|
||||||
}
|
}
|
||||||
setDeferredPrompt(null);
|
setDeferredPrompt(null); // Clear the deferred prompt to manage its lifecycle
|
||||||
|
} else {
|
||||||
|
console.log('No deferred prompt available');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility function for converting base64 string to Uint8Array
|
|
||||||
const base64ToUint8Array = base64 => {
|
|
||||||
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
|
||||||
const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
const rawData = window.atob(b64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribeToNotifications = async (e) => {
|
const subscribeToNotifications = async (e) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!navigator.serviceWorker) {
|
||||||
|
console.error('Service worker is not supported by this browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
if (!registration) {
|
if (!registration) {
|
||||||
console.error('Service worker registration not found.');
|
console.error('Service worker registration not found.');
|
||||||
registration
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
|
||||||
|
if (!vapidPublicKey) {
|
||||||
|
// Fetch the public key from the server if not present in env variables
|
||||||
|
const response = await fetch('/api/notify', { method: 'GET' });
|
||||||
|
const responseData = await response.json();
|
||||||
|
vapidPublicKey = responseData.pk;
|
||||||
|
setSubs(responseData.subs);
|
||||||
|
if (!vapidPublicKey) {
|
||||||
|
throw new Error("Failed to fetch VAPID public key from server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
const sub = await registration.pushManager.subscribe({
|
const sub = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: base64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY)
|
applicationServerKey: common.base64ToUint8Array(vapidPublicKey)
|
||||||
});
|
});
|
||||||
// Call your API to save subscription data on server
|
// Call your API to save subscription data on server
|
||||||
setSubscription(sub);
|
if (session.user?.id != null) {
|
||||||
setIsSubscribed(true);
|
await fetch(`/api/notify`, {
|
||||||
console.log('Web push subscribed!');
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ subscription: sub, id: session.user.id })
|
||||||
|
}).then(async response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save subscription data on server.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Subscription data saved on server.');
|
||||||
|
const s = await response.json();
|
||||||
|
setSubs(s.subs);
|
||||||
|
setSubscription(sub);
|
||||||
|
setIsSubscribed(true);
|
||||||
|
console.log('Web push subscribed!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
console.log(sub);
|
console.log(sub);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error subscribing to notifications:', error);
|
console.error('Error subscribing to notifications:', error);
|
||||||
@ -105,11 +163,34 @@ function PwaManager() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await subscription.unsubscribe();
|
if (subscription) {
|
||||||
// Call your API to delete or invalidate subscription data on server
|
await subscription.unsubscribe();
|
||||||
setSubscription(null);
|
// Call your API to delete or invalidate subscription data on server
|
||||||
setIsSubscribed(false);
|
setSubscription(null);
|
||||||
console.log('Web push unsubscribed!');
|
setIsSubscribed(false);
|
||||||
|
if (session?.user?.id != null) {
|
||||||
|
await fetch(`/api/notify`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
//send the current subscription to be removed
|
||||||
|
body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint })
|
||||||
|
}
|
||||||
|
).then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete subscription data on server.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Subscription data deleted on server.');
|
||||||
|
const s = await response.json();
|
||||||
|
setSubs(s.subs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('Web push unsubscribed!');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error unsubscribing from notifications:', error);
|
console.error('Error unsubscribing from notifications:', error);
|
||||||
}
|
}
|
||||||
@ -118,14 +199,19 @@ function PwaManager() {
|
|||||||
// Function to request push notification permission
|
// Function to request push notification permission
|
||||||
const requestNotificationPermission = async (e) => {
|
const requestNotificationPermission = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const permission = await Notification.requestPermission();
|
if (isSupported()) {
|
||||||
setNotificationPermission(permission);
|
const permission = await Notification.requestPermission();
|
||||||
if (permission === "granted") {
|
setNotificationPermission(permission);
|
||||||
// User granted permission
|
if (permission === "granted") {
|
||||||
subscribeToNotifications(null); // Pass the required argument here
|
// User granted permission
|
||||||
} else {
|
subscribeToNotifications(null); // Pass the required argument here
|
||||||
// User denied or dismissed permission
|
} else {
|
||||||
console.log("Push notifications permission denied.");
|
// User denied or dismissed permission
|
||||||
|
console.log("Push notifications permission denied.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error('Web push not supported');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,49 +246,132 @@ function PwaManager() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
async function sendTestReminder(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||||||
<>
|
event.preventDefault();
|
||||||
|
if (!subscription) {
|
||||||
|
console.error('Web push not subscribed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch('/api/notify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ broadcast: true, message: 'Моля, въведете вашите предпочитания за юни до 25-ти май.' })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTestCoverMe(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!subscription) {
|
||||||
|
console.error('Web push not subscribed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch('/api/notify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
broadcast: true, message: "Брат ТЕСТ търси заместник за 24-ти май от 10:00 ч. Можеш ли да го покриеш?",
|
||||||
|
//use fontawesome icons for actions
|
||||||
|
actions: [{ action: 'covermeaccepted', title: 'Да ', icon: '✅' }]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAllSubscriptions(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
await fetch(`/api/notify`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
//send the current subscription to be removed
|
||||||
|
body: JSON.stringify({ id: session.user.id })
|
||||||
|
}
|
||||||
|
).then(async response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete subscription data on server.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('ALL subscriptions data deleted on server.');
|
||||||
|
if (subscription) {
|
||||||
|
await subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
setSubs("");
|
||||||
|
setSubscription(null);
|
||||||
|
setIsSubscribed(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isSupported()) {
|
||||||
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>PWA Manager</h1>
|
<p>Това устройство не поддържа нотификации</p>
|
||||||
{!isStandAlone && !isPWAInstalled && (
|
|
||||||
<button
|
|
||||||
onClick={installPWA}
|
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white text-xs py-1 px-2 rounded-full focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
|
|
||||||
>
|
|
||||||
Install PWA
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{isPWAInstalled && <p>App is installed!</p>}
|
|
||||||
{isStandAlone && <p>PWA App</p>}
|
|
||||||
<button
|
|
||||||
onClick={subscribeToNotifications}
|
|
||||||
disabled={isSubscribed}
|
|
||||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-green-500 hover:bg-green-700 text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Subscribe to Notifications
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={unsubscribeFromNotifications}
|
|
||||||
disabled={!isSubscribed}
|
|
||||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-red-500 hover:bg-red-700 text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Unsubscribe from Notifications
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
);
|
||||||
<button
|
}
|
||||||
onClick={sendTestNotification}
|
else {
|
||||||
disabled={!isSubscribed}
|
return (
|
||||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
<>
|
||||||
}`}
|
<div>
|
||||||
>
|
<h1>{isAdmin && " PWA (admin)"}</h1>
|
||||||
Send Test Notification
|
{!isStandAlone && !isPWAInstalled && (
|
||||||
</button>
|
<button
|
||||||
|
onClick={installPWA}
|
||||||
|
className="bg-blue-500 hover:bg-blue-700 text-white text-xs py-1 px-2 rounded-full focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
|
||||||
|
>
|
||||||
|
Инсталирай приложението
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isPWAInstalled && <p>Инсталирано!</p>}
|
||||||
|
{/* {isStandAlone && <p>PWA App</p>} */}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={isSubscribed ? unsubscribeFromNotifications : subscribeToNotifications}
|
||||||
|
disabled={false} // Since the button itself acts as a toggle, the disabled attribute might not be needed
|
||||||
|
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${isSubscribed ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'}`} >
|
||||||
|
{isSubscribed ? 'Спри известията' : 'Показвай известия'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={deleteAllSubscriptions}
|
||||||
|
className="text-xs py-1 px-2 rounded-full focus:outline-none bg-red-500 hover:bg-red-700 text-white"
|
||||||
|
>
|
||||||
|
Спри известията на всички мои устройства {_subs != "" ? `(${_subs})` : ""}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{isAdmin &&
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={sendTestNotification}
|
||||||
|
disabled={!isSubscribed}
|
||||||
|
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={sendTestReminder}
|
||||||
|
disabled={!isSubscribed}
|
||||||
|
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Broadcast Reminder
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={sendTestCoverMe}
|
||||||
|
disabled={!isSubscribed}
|
||||||
|
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Broadcast CoverMe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{notificationPermission !== "granted" && (
|
{notificationPermission !== "granted" && (
|
||||||
<button
|
<button
|
||||||
onClick={togglePushNotifications}
|
onClick={togglePushNotifications}
|
||||||
@ -212,21 +381,32 @@ function PwaManager() {
|
|||||||
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
|
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4" target="_blank">
|
|
||||||
<span className="align-middle">Apple sign-in</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
{isAdmin && <div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4 bg-gray-100 button" target="_blank">
|
||||||
|
<span className="align-middle">Apple sign-in</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PwaManager;
|
export default PwaManager;
|
||||||
|
|
||||||
|
//get server side props - subs count
|
||||||
|
export const getServerSideProps = async (context) => {
|
||||||
|
//ToDo: get the number of subscriptions from the database
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
subs: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
133
components/PwaManagerNotifications.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import common from '../src/helpers/common'; // Ensure this path is correct
|
||||||
|
import { set } from 'date-fns';
|
||||||
|
|
||||||
|
function PwaManagerNotifications() {
|
||||||
|
const [isPermissionGranted, setIsPermissionGranted] = useState(false);
|
||||||
|
const [subscription, setSubscription] = useState(null);
|
||||||
|
const [registration, setRegistration] = useState(null);
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
// Check if all required APIs are supported
|
||||||
|
const isSupported = () =>
|
||||||
|
'Notification' in window &&
|
||||||
|
'serviceWorker' in navigator &&
|
||||||
|
'PushManager' in window;
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSupported()) {
|
||||||
|
requestNotificationPermission(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestNotificationPermission = async (e) => {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (Notification.permission === 'denied') {
|
||||||
|
console.log('Notification permission denied.');
|
||||||
|
alert('Известията са забранени. Моля, разрешете известията от браузъра.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsPermissionGranted(Notification.permission === 'granted');
|
||||||
|
if (Notification.permission === 'default') {
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
if (permission === 'granted') {
|
||||||
|
console.log('Notification permission granted.');
|
||||||
|
getSubscription();
|
||||||
|
} else {
|
||||||
|
console.log('Notification permission denied.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
getSubscription();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getSubscription = async () => {
|
||||||
|
// Handle Push Notification Subscription
|
||||||
|
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.pushManager.getSubscription().then(existingSubscription => {
|
||||||
|
if (existingSubscription) {
|
||||||
|
console.log('Already subscribed.');
|
||||||
|
setSubscription(existingSubscription);
|
||||||
|
} else if (Notification.permission === "granted") {
|
||||||
|
// Permission was already granted but no subscription exists, so subscribe now
|
||||||
|
subscribeToNotifications(registration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribeToNotifications = async () => {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
let vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; // Ensure this is configured
|
||||||
|
if (!vapidPublicKey) {
|
||||||
|
// Fetch the public key from the server if not present in env variables
|
||||||
|
const response = await fetch('/api/notify', { method: 'GET' });
|
||||||
|
const responseData = await response.json();
|
||||||
|
vapidPublicKey = responseData.pk;
|
||||||
|
if (!vapidPublicKey) {
|
||||||
|
throw new Error("Failed to fetch VAPID public key from server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const convertedVapidKey = common.base64ToUint8Array(vapidPublicKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: convertedVapidKey
|
||||||
|
});
|
||||||
|
console.log('Subscribed to push notifications:', subscription);
|
||||||
|
setSubscription(subscription);
|
||||||
|
sendSubscriptionToServer(subscription);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to subscribe to push notifications:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendSubscriptionToServer = async (sub) => {
|
||||||
|
if (session.user?.id != null) {
|
||||||
|
await fetch(`/api/notify`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ subscription: sub, id: session.user.id })
|
||||||
|
}).then(async response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save subscription data on server.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Subscription data saved on server.');
|
||||||
|
const s = await response.json();
|
||||||
|
setSubscription(sub);
|
||||||
|
console.log('Web push subscribed!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={isPermissionGranted ? undefined : requestNotificationPermission}
|
||||||
|
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out
|
||||||
|
${isPermissionGranted ?
|
||||||
|
'border border-blue-300 text-blue-300 bg-transparent hover:bg-blue-100'
|
||||||
|
: 'bg-blue-400 text-white'
|
||||||
|
}`}
|
||||||
|
disabled={isPermissionGranted}
|
||||||
|
>
|
||||||
|
{!isSupported() ? "не поддъжа известия" : (isPermissionGranted && subscription ? 'Известията включени' : 'Включи известията')}
|
||||||
|
</button>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default PwaManagerNotifications;
|
@ -10,7 +10,10 @@ 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
|
//todo import Availability type from prisma schema
|
||||||
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns';
|
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; //ToDo obsolete
|
||||||
|
import { stat } from 'fs';
|
||||||
|
|
||||||
|
const { DateTime, FixedOffsetZone } = require('luxon');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -19,7 +22,7 @@ const fetchConfig = async () => {
|
|||||||
return config.default;
|
return config.default;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, datePicker = false }) {
|
export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, cartEvent, datePicker = false }) {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const urls = {
|
const urls = {
|
||||||
@ -65,14 +68,16 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Define the minimum and maximum times
|
// get cart event or set default time for Sofia timezone
|
||||||
const minTime = new Date();
|
// const minTime = cartEvent?.startTime || DateTime.now().set({ hour: 8, minute: 0, zone: 'Europe/Sofia' }).toJSDate();
|
||||||
minTime.setHours(9, 0, 0, 0); // 8:00 AM
|
// const maxTime = cartEvent?.endTime || DateTime.now().set({ hour: 20, minute: 0, zone: 'Europe/Sofia' }).toJSDate();
|
||||||
const maxTime = new Date();
|
const d = DateTime.fromJSDate(day).setZone('Europe/Sofia', { keepLocalTime: true });
|
||||||
maxTime.setHours(19, 30, 0, 0); // 8:00 PM
|
const minTime = d.set({ hour: 9, minute: 0 }).toJSDate();
|
||||||
|
const maxTime = d.set({ hour: 19, minute: 30 }).toJSDate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeSlots(generateTimeSlots(minTime, maxTime, 90, availabilities));
|
setTimeSlots(generateTimeSlots(new Date(minTime), new Date(maxTime), cartEvent?.shiftDuration || 90, availabilities));
|
||||||
|
console.log("AvailabilityForm: minTime: " + common.getTimeFormatted(minTime) + ", maxTime: " + common.getTimeFormatted(maxTime), ", " + cartEvent?.shiftDuration || 90 + " min. shifts", cartEvent ? "cartEvent" : "cartEvent MISSING!!!");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
@ -187,15 +192,14 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
|
|
||||||
// Common function to set shared properties
|
// Common function to set shared properties
|
||||||
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
||||||
let startTime = new Date(availability.startTime || day);
|
const d = DateTime.fromJSDate(day).setZone('Europe/Sofia', { keepLocalTime: true });
|
||||||
startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0);
|
console.log("day: " + d.toISODate());
|
||||||
|
let startTime = common.setTime(d, group[0].startTime).toJSDate();
|
||||||
let endTime = new Date(availability.endTime || day);
|
let endTime = common.setTime(d, group[group.length - 1].endTime).toJSDate();
|
||||||
endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0);
|
|
||||||
|
|
||||||
availability.startTime = startTime;
|
availability.startTime = startTime;
|
||||||
availability.endTime = endTime;
|
availability.endTime = endTime;
|
||||||
availability.name = common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime);
|
availability.name = common.getTimeFormatted(group[0].startTime) + "-" + common.getTimeFormatted(group[group.length - 1].endTime);
|
||||||
|
|
||||||
availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
|
availability.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;
|
||||||
@ -209,7 +213,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
} else {
|
} else {
|
||||||
availability.type = "OneTime"
|
availability.type = "OneTime"
|
||||||
availability.repeatWeekly = false;
|
availability.repeatWeekly = false;
|
||||||
availability.dayOfMonth = startTime.getDate();
|
availability.dayOfMonth = availability.startTime.getDate();
|
||||||
availability.endDate = null;
|
availability.endDate = null;
|
||||||
}
|
}
|
||||||
availability.isFromPreviousMonth = false;
|
availability.isFromPreviousMonth = false;
|
||||||
@ -285,28 +289,17 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
const generateTimeSlots = (start, end, increment, items) => {
|
const generateTimeSlots = (start, end, increment, items) => {
|
||||||
const slots = [];
|
const slots = [];
|
||||||
let currentTime = start;
|
let currentTime = start;
|
||||||
|
|
||||||
const baseDate = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
|
|
||||||
|
|
||||||
while (isBefore(currentTime, end)) {
|
while (isBefore(currentTime, end)) {
|
||||||
let slotStart = normalizeTime(currentTime, baseDate);
|
let slotStart = currentTime;
|
||||||
let slotEnd = normalizeTime(addMinutes(currentTime, increment), baseDate);
|
let slotEnd = addMinutes(currentTime, increment);
|
||||||
|
|
||||||
const isChecked = items.some(item => {
|
const isChecked = items.some(item => {
|
||||||
let itemStart = item.startTime ? normalizeTime(new Date(item.startTime), baseDate) : null;
|
return item.startTime && item.endTime &&
|
||||||
let itemEnd = item.endTime ? normalizeTime(new Date(item.endTime), baseDate) : null;
|
common.isTimeBetween(item.startTime, item.endTime, slotStart) &&
|
||||||
|
common.isTimeBetween(item.startTime, item.endTime, slotEnd);
|
||||||
return itemStart && itemEnd &&
|
|
||||||
(slotStart.getTime() < itemEnd.getTime()) &&
|
|
||||||
(slotEnd.getTime() > itemStart.getTime());
|
|
||||||
});
|
|
||||||
|
|
||||||
slots.push({
|
|
||||||
startTime: slotStart,
|
|
||||||
endTime: slotEnd,
|
|
||||||
isChecked: isChecked,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
slots.push({ startTime: slotStart, endTime: slotEnd, isChecked: isChecked, });
|
||||||
currentTime = addMinutes(currentTime, increment);
|
currentTime = addMinutes(currentTime, increment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,16 +313,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
return slots;
|
return slots;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Normalize the time part of a date by using a base date
|
|
||||||
function normalizeTime(date, baseDate) {
|
|
||||||
return set(baseDate, {
|
|
||||||
hours: getHours(date),
|
|
||||||
minutes: getMinutes(date),
|
|
||||||
seconds: getSeconds(date),
|
|
||||||
milliseconds: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
||||||
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
||||||
const handleAllDayChange = (e) => {
|
const handleAllDayChange = (e) => {
|
||||||
@ -390,7 +373,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
<span className="checkmark"></span>
|
<span className="checkmark"></span>
|
||||||
</label>
|
</label>
|
||||||
{slots.map((slot, index) => {
|
{slots.map((slot, index) => {
|
||||||
const slotLabel = `${common.getTimeFomatted(slot.startTime)} до ${common.getTimeFomatted(slot.endTime)}`;
|
const slotLabel = `${common.getTimeFormatted(slot.startTime)} до ${common.getTimeFormatted(slot.endTime)}`;
|
||||||
slot.transportNeeded = slot.isFirst || slot.isLast;
|
slot.transportNeeded = slot.isFirst || slot.isLast;
|
||||||
// Determine if the current slot is the first or the last
|
// Determine if the current slot is the first or the last
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
|
|||||||
|
|
||||||
if (!availability.name) {
|
if (!availability.name) {
|
||||||
// availability.name = "От календара";
|
// availability.name = "От календара";
|
||||||
availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
|
availability.name = common.getTimeFormatted(availability.startTime) + "-" + common.getTimeFormatted(availability.endTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
availability.dayofweek = common.getDayOfWeekNameEnEnumForDate(availability.startTime);
|
availability.dayofweek = common.getDayOfWeekNameEnEnumForDate(availability.startTime);
|
||||||
|
@ -173,6 +173,9 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
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;
|
ass.canTransport = av.isWithTransportIn || av.isWithTransportOut;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
borderStyles += 'border-l-4 border-red-500 ';
|
||||||
|
}
|
||||||
|
|
||||||
if (publisherInfo.hasUpToDateAvailabilities) {
|
if (publisherInfo.hasUpToDateAvailabilities) {
|
||||||
//add green right border
|
//add green right border
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, use } from 'react';
|
||||||
import { Calendar, momentLocalizer, dateFnsLocalizer } from 'react-big-calendar';
|
import { Calendar, momentLocalizer, dateFnsLocalizer } from 'react-big-calendar';
|
||||||
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
||||||
import AvailabilityForm from '../availability/AvailabilityForm';
|
import AvailabilityForm from '../availability/AvailabilityForm';
|
||||||
@ -9,7 +9,7 @@ import common from '../../src/helpers/common';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import moment from 'moment';
|
import moment from 'moment'; // ToDo: obsolete, remove it
|
||||||
import 'moment/locale/bg'; // Import Bulgarian locale
|
import 'moment/locale/bg'; // Import Bulgarian locale
|
||||||
import { ArrowLeftCircleIcon } from '@heroicons/react/24/outline';
|
import { ArrowLeftCircleIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
@ -18,11 +18,13 @@ import { MdToday } from 'react-icons/md';
|
|||||||
|
|
||||||
import { useSwipeable } from 'react-swipeable';
|
import { useSwipeable } from 'react-swipeable';
|
||||||
import axiosInstance from '../../src/axiosSecure';
|
import axiosInstance from '../../src/axiosSecure';
|
||||||
|
import { set } from 'date-fns';
|
||||||
|
import { get } from 'http';
|
||||||
|
|
||||||
// import { set, format, addDays } from 'date-fns';
|
// import { set, format, addDays } from 'date-fns';
|
||||||
// import { isEqual, isSameDay, getHours, getMinutes } from 'date-fns';
|
// import { isEqual, isSameDay, getHours, getMinutes } from 'date-fns';
|
||||||
import { filter } from 'jszip';
|
// import { filter } from 'jszip';
|
||||||
import e from 'express';
|
// import e from 'express';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -46,9 +48,19 @@ const messages = {
|
|||||||
// Any other labels you want to translate...
|
// Any other labels you want to translate...
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
const AvCalendar = ({ publisherId, events, selectedDate, cartEvents, lastPublishedDate }) => {
|
||||||
|
const [editLockedBefore, setEditLockedBefore] = useState(new Date(lastPublishedDate));
|
||||||
const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
setIsAdmin(await ProtectedRoute.IsInRole(UserRole.ADMIN));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check admin role:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
//const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
||||||
|
|
||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
//ToDo: see if we can optimize this
|
//ToDo: see if we can optimize this
|
||||||
@ -65,7 +77,18 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
return { start, end };
|
return { start, end };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [cartEvent, setCartEvent] = useState(null);
|
||||||
|
function getCartEvent(date) {
|
||||||
|
const dayOfWeek = common.getDayOfWeekNameEnEnumForDate(date);
|
||||||
|
const ce = cartEvents?.find(e => e.dayofweek === dayOfWeek);
|
||||||
|
return ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("useEffect: ", date, selectedEvents, cartEvents);
|
||||||
|
setCartEvent(getCartEvent(date));
|
||||||
|
},
|
||||||
|
[date, selectedEvents]);
|
||||||
|
|
||||||
// Update internal state when `events` prop changes
|
// Update internal state when `events` prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -113,6 +136,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
//setDisplayedEvents(evts);
|
//setDisplayedEvents(evts);
|
||||||
}, [visibleRange, evts, currentView]);
|
}, [visibleRange, evts, currentView]);
|
||||||
|
|
||||||
|
// todo: review that
|
||||||
const handlers = useSwipeable({
|
const handlers = useSwipeable({
|
||||||
onSwipedLeft: () => navigate('NEXT'),
|
onSwipedLeft: () => navigate('NEXT'),
|
||||||
onSwipedRight: () => navigate('PREV'),
|
onSwipedRight: () => navigate('PREV'),
|
||||||
@ -201,50 +225,39 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
return existingEvents;
|
return existingEvents;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define min and max times
|
|
||||||
const minHour = 8; // 8:00 AM
|
// const totalHours = maxHour - minHour;
|
||||||
const maxHour = 20; // 8:00 PM
|
|
||||||
const minTime = new Date();
|
|
||||||
minTime.setHours(minHour, 0, 0);
|
|
||||||
const maxTime = new Date();
|
|
||||||
maxTime.setHours(maxHour, 0, 0);
|
|
||||||
const totalHours = maxHour - minHour;
|
|
||||||
|
|
||||||
const handleSelect = ({ mode, start, end }) => {
|
const handleSelect = ({ mode, start, end }) => {
|
||||||
const startdate = typeof start === 'string' ? new Date(start) : start;
|
//we set the time to proper timezone
|
||||||
const enddate = typeof end === 'string' ? new Date(end) : end;
|
const startdate = common.setTimezone(start);
|
||||||
|
const enddate = common.setTimezone(end);
|
||||||
|
|
||||||
if (!start || !end) return;
|
if (!start || !end) return;
|
||||||
//readonly for past dates (ToDo: if not admin)
|
//readonly for past dates (ToDo: if not admin)
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
if (startdate < new Date() || end < new Date() || startdate > end) return;
|
if (startdate < new Date() || end < new Date() || startdate > end) return;
|
||||||
|
//or if schedule is published (lastPublishedDate)
|
||||||
|
if (editLockedBefore && startdate < editLockedBefore) {
|
||||||
|
toast.error(`Не можете да променяте предпочитанията си за дати преди ${common.getDateFormatedShort(editLockedBefore)}.`, { autoClose: 5000 });
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Check if start and end are on the same day
|
// Check if start and end are on the same day
|
||||||
if (startdate.toDateString() !== enddate.toDateString()) {
|
if (startdate.toDateString() !== enddate.toDateString()) {
|
||||||
end = common.setTimeHHmm(startdate, "23:59");
|
end = common.setTimeHHmm(startdate, "23:59");
|
||||||
}
|
}
|
||||||
|
|
||||||
const startMinutes = common.getTimeInMinutes(start);
|
// Update date state and calculate events based on the new startdate
|
||||||
const endMinutes = common.getTimeInMinutes(end);
|
setDate(startdate);
|
||||||
|
const existingEvents = filterEvents(evts, publisherId, startdate);
|
||||||
|
console.log("handleSelect: ", existingEvents);
|
||||||
|
|
||||||
// Adjust start and end times to be within min and max hours
|
// Use the updated startdate for getCartEvent and ensure it reflects in the state properly
|
||||||
if (startMinutes < common.getTimeInMinutes(common.setTimeHHmm(start, minHour))) {
|
const cartEvent = getCartEvent(startdate);
|
||||||
start = common.setTimeHHmm(start, minHour);
|
setCartEvent(cartEvent);
|
||||||
}
|
console.log("cartEvent: ", cartEvent);
|
||||||
if (endMinutes > common.getTimeInMinutes(common.setTimeHHmm(end, maxHour))) {
|
|
||||||
end = common.setTimeHHmm(end, maxHour);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDate(start);
|
|
||||||
|
|
||||||
// get exising events for the selected date
|
|
||||||
//ToDo: properly fix this. filterEvents does not return the expcted results
|
|
||||||
let existingEvents = filterEvents(evts, publisherId, startdate);
|
|
||||||
// if existingEvents is empty - create new with the selected range
|
|
||||||
// if (existingEvents.length === 0) {
|
|
||||||
// existingEvents = [{ startTime: start, endTime: end }];
|
|
||||||
// }
|
|
||||||
console.log("handleSelect: " + existingEvents);
|
|
||||||
setSelectedEvents(existingEvents);
|
setSelectedEvents(existingEvents);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
@ -353,15 +366,15 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
bgColor = event.isBySystem ? "bg-red-500" : (event.isConfirmed || true ? "bg-green-500" : "bg-yellow-500");
|
bgColor = event.isBySystem ? "bg-red-500" : (event.isConfirmed || true ? "bg-green-500" : "bg-yellow-500");
|
||||||
|
|
||||||
//event.title = event.publisher.name; //ToDo: add other publishers names
|
//event.title = event.publisher.name; //ToDo: add other publishers names
|
||||||
//event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime);
|
//event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime);
|
||||||
} else {
|
} else {
|
||||||
if (event.start !== undefined && event.end !== undefined && event.startTime !== null && event.endTime !== null) {
|
if (event.start !== undefined && event.end !== undefined && event.startTime !== null && event.endTime !== null) {
|
||||||
try {
|
try {
|
||||||
if (event.type === "recurring") {
|
if (event.type === "recurring") {
|
||||||
event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime);
|
event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime);
|
event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@ -509,8 +522,8 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
onSelectSlot={handleSelect}
|
onSelectSlot={handleSelect}
|
||||||
onSelectEvent={handleEventClick}
|
onSelectEvent={handleEventClick}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
min={minTime} // Set minimum time
|
min={cartEvent?.startTime} // Set minimum time
|
||||||
max={maxTime} // Set maximum time
|
max={cartEvent?.endTime} // Set maximum time
|
||||||
messages={messages}
|
messages={messages}
|
||||||
view={currentView}
|
view={currentView}
|
||||||
views={['month', 'week', 'agenda']}
|
views={['month', 'week', 'agenda']}
|
||||||
@ -530,6 +543,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
showAllEvents={true}
|
showAllEvents={true}
|
||||||
onNavigate={setDate}
|
onNavigate={setDate}
|
||||||
className="rounded-lg shadow-lg"
|
className="rounded-lg shadow-lg"
|
||||||
|
longPressThreshold={150} // default value 250
|
||||||
/>
|
/>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||||
@ -540,6 +554,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
date={date}
|
date={date}
|
||||||
onDone={handleDialogClose}
|
onDone={handleDialogClose}
|
||||||
inline={true}
|
inline={true}
|
||||||
|
cartEvent={cartEvent}
|
||||||
// Pass other props as needed
|
// Pass other props as needed
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,8 +69,8 @@ export default function CartEventForm(props: IProps) {
|
|||||||
try {
|
try {
|
||||||
console.log("fetching cart event from component " + router.query.id);
|
console.log("fetching cart event from component " + router.query.id);
|
||||||
const { data } = await axiosInstance.get(urls.apiUrl + id);
|
const { data } = await axiosInstance.get(urls.apiUrl + id);
|
||||||
data.startTime = common.formatTimeHHmm(data.startTime)
|
data.startTime = common.getTimeFormatted(data.startTime)
|
||||||
data.endTime = common.formatTimeHHmm(data.endTime)
|
data.endTime = common.getTimeFormatted(data.endTime)
|
||||||
setEvt(data);
|
setEvt(data);
|
||||||
|
|
||||||
console.log("id:" + evt.id);
|
console.log("id:" + evt.id);
|
||||||
|
@ -19,34 +19,6 @@ import { useSession } from "next-auth/react"
|
|||||||
|
|
||||||
// import { Tabs, List } from 'tw-elements'
|
// import { Tabs, List } from 'tw-elements'
|
||||||
|
|
||||||
// model Publisher {
|
|
||||||
// id String @id @default(cuid())
|
|
||||||
// firstName String
|
|
||||||
// lastName String
|
|
||||||
// email String @unique
|
|
||||||
// phone String?
|
|
||||||
// isActive Boolean @default(true)
|
|
||||||
// isImported Boolean @default(false)
|
|
||||||
// age Int?
|
|
||||||
// availabilities Availability[]
|
|
||||||
// assignments Assignment[]
|
|
||||||
|
|
||||||
// emailVerified DateTime?
|
|
||||||
// accounts Account[]
|
|
||||||
// sessions Session[]
|
|
||||||
// role UserRole @default(USER)
|
|
||||||
// desiredShiftsPerMonth Int @default(4)
|
|
||||||
// isMale Boolean @default(true)
|
|
||||||
// isNameForeign Boolean @default(false)
|
|
||||||
|
|
||||||
// familyHeadId String? // Optional familyHeadId for each family member
|
|
||||||
// familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
|
|
||||||
// familyMembers Publisher[] @relation("FamilyMember")
|
|
||||||
// type PublisherType @default(Publisher)
|
|
||||||
// Town String?
|
|
||||||
// Comments String?
|
|
||||||
// }
|
|
||||||
|
|
||||||
Array.prototype.groupBy = function (prop) {
|
Array.prototype.groupBy = function (prop) {
|
||||||
return this.reduce(function (groups, item) {
|
return this.reduce(function (groups, item) {
|
||||||
const val = item[prop]
|
const val = item[prop]
|
||||||
@ -59,9 +31,11 @@ Array.prototype.groupBy = function (prop) {
|
|||||||
export default function PublisherForm({ item, me }) {
|
export default function PublisherForm({ item, me }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
|
const [congregations, setCongregations] = useState([]);
|
||||||
|
|
||||||
const urls = {
|
const urls = {
|
||||||
apiUrl: "/api/data/publishers/",
|
apiUrl: "/api/data/publishers/",
|
||||||
|
congregationsUrl: "/api/data/congregations",
|
||||||
indexUrl: session?.user?.role == UserRole.ADMIN ? "/cart/publishers" : "/dash"
|
indexUrl: session?.user?.role == UserRole.ADMIN ? "/cart/publishers" : "/dash"
|
||||||
}
|
}
|
||||||
console.log("urls.indexUrl: " + urls.indexUrl);
|
console.log("urls.indexUrl: " + urls.indexUrl);
|
||||||
@ -72,6 +46,9 @@ export default function PublisherForm({ item, me }) {
|
|||||||
const h = (await import("../../src/helpers/const.js")).default;
|
const h = (await import("../../src/helpers/const.js")).default;
|
||||||
//console.log("fetchModules: " + JSON.stringify(h));
|
//console.log("fetchModules: " + JSON.stringify(h));
|
||||||
setHelper(h);
|
setHelper(h);
|
||||||
|
|
||||||
|
const response = await axiosInstance.get(urls.congregationsUrl);
|
||||||
|
setCongregations(response.data);
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModules();
|
fetchModules();
|
||||||
@ -113,15 +90,17 @@ export default function PublisherForm({ item, me }) {
|
|||||||
publisher.availabilities = undefined;
|
publisher.availabilities = undefined;
|
||||||
publisher.assignments = undefined;
|
publisher.assignments = undefined;
|
||||||
|
|
||||||
let { familyHeadId, userId, ...rest } = publisher;
|
let { familyHeadId, userId, congregationId, ...rest } = publisher;
|
||||||
// Set the familyHead relation based on the selected head
|
// Set the familyHead relation based on the selected head
|
||||||
const familyHeadRelation = familyHeadId ? { connect: { id: familyHeadId } } : { disconnect: true };
|
const familyHeadRelation = familyHeadId ? { connect: { id: familyHeadId } } : { disconnect: true };
|
||||||
const userRel = userId ? { connect: { id: userId } } : { disconnect: true };
|
const userRel = userId ? { connect: { id: userId } } : { disconnect: true };
|
||||||
|
const congregationRel = congregationId ? { connect: { id: parseInt(congregationId) } } : { disconnect: true };
|
||||||
// Return the new state without familyHeadId and with the correct familyHead relation
|
// Return the new state without familyHeadId and with the correct familyHead relation
|
||||||
rest = {
|
rest = {
|
||||||
...rest,
|
...rest,
|
||||||
familyHead: familyHeadRelation,
|
familyHead: familyHeadRelation,
|
||||||
user: userRel
|
user: userRel,
|
||||||
|
congregation: congregationRel
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -247,14 +226,77 @@ export default function PublisherForm({ item, me }) {
|
|||||||
<input type="text" id="town" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
<input type="text" id="town" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{/* notifications */}
|
<label className="label" htmlFor="congregationId">Сбор</label>
|
||||||
<div className="form-check">
|
<select id="congregationId" name="congregationId" value={publisher.congregationId} onChange={handleChange} className="select textbox" placeholder="Община" autoFocus >
|
||||||
<input className="checkbox" type="checkbox" id="isSubscribedToCoverMe" name="isSubscribedToCoverMe" onChange={handleChange} checked={publisher.isSubscribedToCoverMe} autoComplete="off" />
|
<option value="">Избери сбор</option>
|
||||||
<label className="label" htmlFor="isSubscribedToCoverMe">Абониран за имейли за заместване</label>
|
{congregations.map((congregation) => (
|
||||||
<input className="checkbox" type="checkbox" id="isSubscribedToReminders" name="isSubscribedToReminders" onChange={handleChange} checked={publisher.isSubscribedToReminders} autoComplete="off" />
|
<option key={congregation.id} value={congregation.id}>
|
||||||
<label className="label" htmlFor="isSubscribedToReminders">Абониран за напомняния (имейл)</label>
|
{congregation.name}
|
||||||
{/* prompt to install PWA */}
|
</option>
|
||||||
</div>
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* notifications */}
|
||||||
|
<div className="mb-6 p-4 border border-gray-300 rounded-lg">
|
||||||
|
<fieldset>
|
||||||
|
<legend className="text-lg font-medium mb-2">Известия</legend>
|
||||||
|
|
||||||
|
{/* Email notifications group */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Известия по имейл</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
className="checkbox cursor-not-allowed opacity-50"
|
||||||
|
type="checkbox"
|
||||||
|
id="isSubscribedToCoverMeMandatory"
|
||||||
|
name="isSubscribedToCoverMeMandatory"
|
||||||
|
onChange={handleChange} // This will not fire due to being disabled, but kept for consistency
|
||||||
|
checked={true} // Always checked
|
||||||
|
disabled={true} // User cannot change this field
|
||||||
|
autoComplete="off" />
|
||||||
|
<label className="label cursor-not-allowed opacity-50" htmlFor="isSubscribedToCoverMeMandatory">
|
||||||
|
Имейли за заместване които отговарят на моите предпочитания
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
className="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id="isSubscribedToCoverMe"
|
||||||
|
name="isSubscribedToCoverMe"
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={publisher.isSubscribedToCoverMe}
|
||||||
|
autoComplete="off" />
|
||||||
|
<label className="label" htmlFor="isSubscribedToCoverMe">
|
||||||
|
Всички заявки за заместване
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
className="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id="isSubscribedToReminders"
|
||||||
|
name="isSubscribedToReminders"
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={publisher.isSubscribedToReminders}
|
||||||
|
autoComplete="off" />
|
||||||
|
<label className="label" htmlFor="isSubscribedToReminders">
|
||||||
|
Други напомняния
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* In-App notifications group */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Известия в приложението</h3>
|
||||||
|
<PwaManager />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* button to install PWA */}
|
{/* button to install PWA */}
|
||||||
@ -267,7 +309,7 @@ export default function PublisherForm({ item, me }) {
|
|||||||
{/* ADMINISTRATORS ONLY */}
|
{/* ADMINISTRATORS ONLY */}
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
||||||
<div className="border border-blue-500 border-solid p-2">
|
<div className="border border-blue-500 border-solid p-2">
|
||||||
<PwaManager />
|
{/* prompt to install PWA */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="label" htmlFor="type">Тип</label>
|
<label className="label" htmlFor="type">Тип</label>
|
||||||
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
||||||
@ -326,7 +368,7 @@ export default function PublisherForm({ item, me }) {
|
|||||||
|
|
||||||
{/* save */}
|
{/* save */}
|
||||||
<button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="submit">
|
<button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="submit">
|
||||||
{router.query?.id ? "Update" : "Create"}
|
{router.query?.id ? "Запази" : "Създай"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -11,14 +11,20 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s
|
|||||||
const [searchResults, setSearchResults] = useState([]);
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
const [selectedDate, setSelectedDate] = useState(filterDate);
|
const [selectedDate, setSelectedDate] = useState(filterDate);
|
||||||
|
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// fetchPublishers();
|
||||||
|
// }, []); // Empty dependency array ensures this useEffect runs only once
|
||||||
|
|
||||||
|
// Update publishers when filterDate or showList changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPublishers();
|
fetchPublishers();
|
||||||
}, []); // Empty dependency array ensures this useEffect runs only once
|
}, [filterDate, showList]);
|
||||||
|
|
||||||
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&availabilities=false`;
|
||||||
|
|
||||||
if (filterDate) {
|
if (filterDate) {
|
||||||
url += `&filterDate=${common.getISODateOnly(filterDate)}`;
|
url += `&filterDate=${common.getISODateOnly(filterDate)}`;
|
||||||
@ -60,10 +66,7 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s
|
|||||||
// console.log("filterDate changed = ", filterDate);
|
// console.log("filterDate changed = ", filterDate);
|
||||||
// }, [filterDate]);
|
// }, [filterDate]);
|
||||||
|
|
||||||
// Update publishers when filterDate or showList changes
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPublishers();
|
|
||||||
}, [filterDate, showList]);
|
|
||||||
|
|
||||||
// Update selectedItem when selectedId changes and also at the initial load
|
// Update selectedItem when selectedId changes and also at the initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -85,43 +85,46 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, subscribedPublishers, a
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||||
<div className="absolute inset-0 bg-black opacity-50" onClick={onClose}></div>
|
<div className="absolute inset-0 bg-black opacity-50" onClick={onClose}></div>
|
||||||
<div className="bg-white p-6 rounded-lg shadow-lg z-10">
|
<div className="bg-white p-6 rounded-lg shadow-lg z-10 max-h-screen overflow-y-auto">
|
||||||
<h2 className="text-lg font-semibold mb-4">Можете да изпратите заявка за заместник до следните групи:</h2>
|
<h2 className="text-lg font-semibold mb-4">Можете да изпратите заявка за заместник до следните групи:</h2>
|
||||||
<div className="mb-4">
|
<div className="space-y-1">
|
||||||
<label className="block mb-2">
|
<div className="flex items-center mb-2">
|
||||||
<div className="flex items-center mb-2">
|
<input id="subscribedPublishersCheckbox"
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
className="mr-2 leading-tight"
|
||||||
className="mr-2 leading-tight"
|
checked={selectedGroups.includes('subscribedPublishers')}
|
||||||
checked={selectedGroups.includes('subscribedPublishers')}
|
onChange={() => handleToggleGroup('subscribedPublishers')}
|
||||||
onChange={() => handleToggleGroup('subscribedPublishers')}
|
/>
|
||||||
/>
|
<label htmlFor="subscribedPublishersCheckbox" className="text-sm font-medium">Абонирани:</label>
|
||||||
<span className="text-sm font-medium">Абонирани:</span>
|
</div>
|
||||||
</div>
|
<div className="overflow-y-auto max-h-64">
|
||||||
<div className="flex flex-wrap">
|
<label className="block mb-2">
|
||||||
{subscribedPublishers.map(pub => (
|
<div className="flex flex-wrap">
|
||||||
<span key={pub.id} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">{pub.name}</span>
|
{subscribedPublishers.map(pub => (
|
||||||
))}
|
<span key={pub.id} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">{pub.name}</span>
|
||||||
</div>
|
))}
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
<div className="mb-4">
|
</div>
|
||||||
<label className="block mb-2">
|
<div className="flex items-center mb-2">
|
||||||
<div className="flex items-center mb-2">
|
<input id="availablePublishersCheckbox"
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
className="mr-2 leading-tight"
|
||||||
className="mr-2 leading-tight"
|
checked={selectedGroups.includes('availablePublishers')}
|
||||||
checked={selectedGroups.includes('availablePublishers')}
|
onChange={() => handleToggleGroup('availablePublishers')}
|
||||||
onChange={() => handleToggleGroup('availablePublishers')}
|
/>
|
||||||
/>
|
<label htmlFor="availablePublishersCheckbox" className="text-sm font-medium">На разположение :</label>
|
||||||
<span className="text-sm font-medium">На разположение :</span>
|
</div>
|
||||||
</div>
|
<div className="overflow-y-auto max-h-64">
|
||||||
<div className="flex flex-wrap">
|
<label className="block mb-2">
|
||||||
{availablePublishers.map(pub => (
|
|
||||||
<span key={pub.id} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">{pub.name}</span>
|
<div className="flex flex-wrap">
|
||||||
))}
|
{availablePublishers.map(pub => (
|
||||||
</div>
|
<span key={pub.id} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">{pub.name}</span>
|
||||||
</label>
|
))}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<button
|
<button
|
||||||
|
@ -6,6 +6,7 @@ import sidemenu, { footerMenu } from './sidemenuData.js'; // Move sidemenu data
|
|||||||
import axiosInstance from "src/axiosSecure";
|
import axiosInstance from "src/axiosSecure";
|
||||||
import common from "src/helpers/common";
|
import common from "src/helpers/common";
|
||||||
import LanguageSwitcher from "./languageSwitcher";
|
import LanguageSwitcher from "./languageSwitcher";
|
||||||
|
import PwaManagerNotifications from "./PwaManagerNotifications";
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import ProtectedPage from "pages/examples/protected";
|
import ProtectedPage from "pages/examples/protected";
|
||||||
@ -142,7 +143,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={toggleSidebar}
|
<button onClick={toggleSidebar}
|
||||||
className="fixed top-1 left-4 z-40 m- text-xl bg-white border border-gray-200 p-2 rounded-full shadow-lg focus:outline-none"
|
className="fixed top-1 left-5 z-40 m- text-xl bg-white border border-gray-200 px-3 py-2.5 rounded-full shadow-lg focus:outline-none"
|
||||||
style={{ transform: isSidebarOpen ? `translateX(${sidebarWidth - 64}px)` : 'translateX(-20px)' }}>☰</button>
|
style={{ transform: isSidebarOpen ? `translateX(${sidebarWidth - 64}px)` : 'translateX(-20px)' }}>☰</button>
|
||||||
<aside id="sidenav" ref={sidebarRef}
|
<aside id="sidenav" ref={sidebarRef}
|
||||||
className="px-2 fixed top-0 left-0 z-30 h-screen overflow-y-auto bg-white border-r dark:bg-gray-900 dark:border-gray-700 transition-all duration-300 sm:translate-x-0 w-64"
|
className="px-2 fixed top-0 left-0 z-30 h-screen overflow-y-auto bg-white border-r dark:bg-gray-900 dark:border-gray-700 transition-all duration-300 sm:translate-x-0 w-64"
|
||||||
@ -196,13 +197,15 @@ function UserDetails({ session }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr className="m-0" />
|
<hr className="m-0" />
|
||||||
<div className="flex items-center">
|
<div className="items-center">
|
||||||
{session.user.image && (
|
{session.user.image && (
|
||||||
<img className="object-cover mx-2 rounded-full h-9 w-9" src={session.user.image} alt="avatar" />
|
<img className="object-cover mx-2 rounded-full h-9 w-9" src={session.user.image} alt="avatar" />
|
||||||
)}
|
)}
|
||||||
<div className="ml-3 overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<p className="mx-1 mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{session.user.name}</p>
|
<p className="mx-1 mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{session.user.name}</p>
|
||||||
<p className="mx-1 mt-1 text-sm font-medium text-gray-600 dark:text-gray-400">{session.user.role}</p>
|
<p className="mx-1 mt-1 text-sm font-medium text-gray-600 dark:text-gray-400">{session.user.role}</p>
|
||||||
|
|
||||||
|
<PwaManagerNotifications />
|
||||||
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>
|
<a href="/api/auth/signout" className={styles.button} onClick={(e) => { e.preventDefault(); signOut(); }}>
|
||||||
{/* {t('logout')} */}
|
{/* {t('logout')} */}
|
||||||
изход
|
изход
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
|
//we're currently using next-pwa (which uses GeneraeSW automatically), so we don't need to use workbox-webpack-plugin
|
||||||
|
// const { InjectManifest, GenerateSW } = require('workbox-webpack-plugin');
|
||||||
|
|
||||||
const withPWA = require('next-pwa')({
|
const withPWA = require('next-pwa')({
|
||||||
dest: 'public',
|
dest: 'public',
|
||||||
register: true, // ?
|
register: true, // ?
|
||||||
publicExcludes: ["!_error*.js"], //?
|
publicExcludes: ["!_error*.js"], //?
|
||||||
|
skipWaiting: true,
|
||||||
//disable: process.env.NODE_ENV === 'development',
|
// disable: process.env.NODE_ENV === 'development',
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = withPWA({
|
module.exports = withPWA({
|
||||||
@ -16,29 +18,76 @@ module.exports = withPWA({
|
|||||||
// !! WARN !!
|
// !! WARN !!
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
compress: false,
|
compress: true,
|
||||||
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.APP_ENV,
|
||||||
server: process.env.NEXT_PUBLIC_PUBLIC_URL
|
server: process.env.NEXT_PUBLIC_PUBLIC_URL
|
||||||
},
|
},
|
||||||
webpack(config, { isServer }) {
|
// pwa: {
|
||||||
|
// dest: 'public',
|
||||||
|
// register: true,
|
||||||
|
// publicExcludes: ["!_error*.js"],
|
||||||
|
// disable: process.env.NODE_ENV === 'development',
|
||||||
|
// // sw: './worker/index.js', // Custom service worker file name
|
||||||
|
// },
|
||||||
|
|
||||||
config.optimization.minimize = true,
|
// plugins: [
|
||||||
productionBrowserSourceMaps = true,
|
// // new InjectManifest({
|
||||||
|
// // // These are some common options, and not all are required.
|
||||||
|
// // // Consult the docs for more info.
|
||||||
|
// // //exclude: [/.../, '...'],
|
||||||
|
// // maximumFileSizeToCacheInBytes: 1 * 1024 * 1024,
|
||||||
|
// // swSrc: './worker.js',
|
||||||
|
// // }),
|
||||||
|
// // new GenerateSW({
|
||||||
|
// // //disable all files
|
||||||
|
// // maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||||
|
// // // swSrc: './worker.js',
|
||||||
|
// // }),
|
||||||
|
// ],
|
||||||
|
webpack: (config, { isServer, buildId, dev }) => {
|
||||||
|
// Configure optimization and source maps
|
||||||
|
config.optimization.minimize = !dev;
|
||||||
|
//config.productionBrowserSourceMaps = true;
|
||||||
|
// Enable source maps based on non-production environments
|
||||||
|
if (!dev) {
|
||||||
|
//config.devtool = 'source-map';
|
||||||
|
/* ⨯ TypeError: Invalid character in header content ["Location"]
|
||||||
|
at ServerResponse.setHeader (node:_http_outgoing:655:3)
|
||||||
|
at _res.setHeader (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:481:24)
|
||||||
|
at NodeNextResponse.setHeader (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-http\node.js:74:19)
|
||||||
|
at NodeNextResponse.redirect (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-http\index.js:43:14)
|
||||||
|
at handleRedirect (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:1208:17)
|
||||||
|
at DevServer.renderToResponseWithComponentsImpl (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:1666:23)
|
||||||
|
at async DevServer.renderPageComponent (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:1856:24)
|
||||||
|
at async DevServer.renderToResponseImpl (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:1894:32)
|
||||||
|
at async DevServer.pipeImpl (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:911:25)
|
||||||
|
at async NextNodeServer.handleCatchallRenderRequest (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\next-server.js:271:17)
|
||||||
|
at async DevServer.handleRequestImpl (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\base-server.js:807:17)
|
||||||
|
at async D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\dev\next-dev-server.js:331:20
|
||||||
|
at async Span.traceAsyncFn (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\trace\trace.js:151:20)
|
||||||
|
at async DevServer.handleRequest (D:\DEV\workspace\REPOS\git.d-popov.com\mwhitnessing\node_modules\next\dist\server\dev\next-dev-server.js:328:24) {
|
||||||
|
code: 'ERR_INVALID_CHAR',
|
||||||
|
page: '/cart/publishers/edit/react_devtools_backend_compact.js.map' */
|
||||||
|
}
|
||||||
|
// Add custom fallbacks
|
||||||
|
config.resolve.fallback = { ...config.resolve.fallback, fs: false };
|
||||||
|
|
||||||
config.resolve.fallback = {
|
// InjectManifest configuration
|
||||||
|
if (!isServer) {
|
||||||
|
// config.plugins.push(new InjectManifest({
|
||||||
|
// // swSrc: './worker.js', // Path to source service worker file
|
||||||
|
// // swDest: '/worker.js', // Destination filename in the build output
|
||||||
|
// maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // Adjust as needed
|
||||||
|
// exclude: [/\.map$/, /_error.js$/, /favicon.ico$/] // Customize exclusion patterns
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
// if you miss it, all the other options in fallback, specified
|
// Bundle Analyzer Configuration
|
||||||
// by next.js will be dropped.
|
|
||||||
...config.resolve.fallback,
|
|
||||||
|
|
||||||
fs: false, // the solution
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Only run the bundle analyzer for production builds and when the ANALYZE environment variable is set
|
|
||||||
if (process.env.ANALYZE && !isServer) {
|
if (process.env.ANALYZE && !isServer) {
|
||||||
|
//const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new BundleAnalyzerPlugin({
|
new BundleAnalyzerPlugin({
|
||||||
analyzerMode: 'static',
|
analyzerMode: 'static',
|
||||||
|
926
package-lock.json
generated
11
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "pwwa",
|
"name": "smws",
|
||||||
"version": "1.2.0",
|
"version": "1.2.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "JW PW Web App",
|
"description": "SMWS | ССОМ | Специално Свидетелстване София",
|
||||||
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.d-popov.com/popov/next-cart-app/issues"
|
"url": "https://git.d-popov.com/popov/next-cart-app/issues"
|
||||||
@ -11,7 +11,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"debug": "node server.js",
|
"debug": "node server.js",
|
||||||
"debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js",
|
"debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js",
|
||||||
"nodeenv": "dotenv -e .env.$APP_ENV -- node server.js",
|
"start-env": "dotenv -e .env.$APP_ENV -- node server.js",
|
||||||
|
"run-env": "dotenv -e .env.$APP_ENV -- npm run build && dotenv -e .env.$APP_ENV -- npm run start",
|
||||||
"prod": "dotenv -e .env.production -- node server.js",
|
"prod": "dotenv -e .env.production -- node server.js",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"buildWin": "npm run build",
|
"buildWin": "npm run build",
|
||||||
@ -66,6 +67,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"levenshtein-edit-distance": "^3.0.1",
|
"levenshtein-edit-distance": "^3.0.1",
|
||||||
|
"luxon": "^3.4.4",
|
||||||
"mailtrap": "^3.3.0",
|
"mailtrap": "^3.3.0",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
@ -106,6 +108,7 @@
|
|||||||
"webpack-bundle-analyzer": "^4.10.1",
|
"webpack-bundle-analyzer": "^4.10.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
|
"workbox-webpack-plugin": "^7.1.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",
|
||||||
"xlsx-style": "^0.8.13",
|
"xlsx-style": "^0.8.13",
|
||||||
"xml-js": "^1.6.11",
|
"xml-js": "^1.6.11",
|
||||||
|
@ -26,9 +26,47 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
|||||||
// appleWebApp: true,
|
// appleWebApp: true,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// (custom) Service worker registration and push notification logic
|
||||||
|
// function registerServiceWorkerAndPushNotifications() {
|
||||||
|
// useEffect(() => {
|
||||||
|
// const registerServiceWorker = async () => {
|
||||||
|
// if ('serviceWorker' in navigator) {
|
||||||
|
// try {
|
||||||
|
// const registration = await navigator.serviceWorker.register('/worker/index.js')
|
||||||
|
// .then((registration) => console.log('reg: ', registration));
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log('Service Worker registration failed:', error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const askForNotificationPermission = async () => {
|
||||||
|
// if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||||
|
// try {
|
||||||
|
// const permission = await Notification.requestPermission();
|
||||||
|
// if (permission === 'granted') {
|
||||||
|
// console.log('Notification permission granted.');
|
||||||
|
// // TODO: Subscribe the user to push notifications here
|
||||||
|
// } else {
|
||||||
|
// console.log('Notification permission not granted.');
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error during service worker registration:', error);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// console.log('Service Worker or Push notifications not supported in this browser.');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// registerServiceWorker();
|
||||||
|
// askForNotificationPermission();
|
||||||
|
// }, []);
|
||||||
|
// }
|
||||||
|
|
||||||
//function SmwsApp({ Component, pageProps: { locale, messages, session, ...pageProps }, }: AppProps<{ session: Session }>) {
|
//function SmwsApp({ Component, pageProps: { locale, messages, session, ...pageProps }, }: AppProps<{ session: Session }>) {
|
||||||
function SmwsApp({ Component, pageProps, session, locale, messages }) {
|
function SmwsApp({ Component, pageProps, session, locale, messages }) {
|
||||||
|
//registerServiceWorkerAndPushNotifications();
|
||||||
|
|
||||||
// dynamic locale loading using our API endpoint
|
// dynamic locale loading using our API endpoint
|
||||||
// const [locale, setLocale] = useState(_locale);
|
// const [locale, setLocale] = useState(_locale);
|
||||||
// const [messages, setMessages] = useState(_messages);
|
// const [messages, setMessages] = useState(_messages);
|
||||||
|
@ -14,8 +14,7 @@ class MyDocument extends Document {
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="CCOM" />
|
<meta name="apple-mobile-web-app-title" content="CCOM" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon.ico"></link>
|
||||||
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
|
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
@ -45,15 +45,22 @@ export const authOptions: NextAuthOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// AppleProvider({
|
AppleProvider({
|
||||||
// clientId: process.env.APPLE_APP_ID,
|
// clientId: process.env.APPLE_APP_ID,
|
||||||
// clientSecret: process.env.APPLE_SECRET
|
// clientSecret: process.env.APPLE_SECRET
|
||||||
// }),
|
clientId: process.env.APPLE_APP_ID,
|
||||||
// AzureADProvider({
|
clientSecret: {
|
||||||
// clientId: process.env.AZURE_AD_CLIENT_ID,
|
appleId: process.env.APPLE_APP_ID,
|
||||||
// clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
teamId: process.env.APPLE_TEAM_ID,
|
||||||
// tenantId: process.env.AZURE_AD_TENANT_ID,
|
privateKey: process.env.APPLE_PK,
|
||||||
// }),
|
keyId: process.env.APPLE_KEY_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({
|
||||||
id: 'credentials',
|
id: 'credentials',
|
||||||
// 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...')
|
||||||
@ -79,9 +86,9 @@ export const authOptions: NextAuthOptions = {
|
|||||||
// // Return null if user data could not be retrieved
|
// // Return null if user data could not be retrieved
|
||||||
// return null
|
// return null
|
||||||
const users = [
|
const users = [
|
||||||
{ id: "1", name: "admin", email: "admin@example.com", password: "admin123", role: "ADMIN" },
|
{ id: "1", name: "admin", email: "admin@example.com", password: "admin123", role: "ADMIN", static: true },
|
||||||
{ id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN" },
|
{ id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN", static: true },
|
||||||
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN" }
|
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN", static: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
const user = users.find(user =>
|
const user = users.find(user =>
|
||||||
@ -167,6 +174,10 @@ export const authOptions: NextAuthOptions = {
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
// https://codevoweb.com/implement-authentication-with-nextauth-in-nextjs-14/
|
// https://codevoweb.com/implement-authentication-with-nextauth-in-nextjs-14/
|
||||||
async signIn({ user, account, profile }) {
|
async signIn({ user, account, profile }) {
|
||||||
|
if (account.provider === 'credentials' && user?.static) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var prisma = common.getPrismaClient();
|
var prisma = common.getPrismaClient();
|
||||||
|
|
||||||
console.log("[nextauth] signIn:", account.provider, user.email)
|
console.log("[nextauth] signIn:", account.provider, user.email)
|
||||||
@ -240,7 +251,10 @@ export const authOptions: NextAuthOptions = {
|
|||||||
session.user.role = token.role;
|
session.user.role = token.role;
|
||||||
session.user.name = token.name || token.email;
|
session.user.name = token.name || token.email;
|
||||||
}
|
}
|
||||||
|
if (user?.impersonating) {
|
||||||
|
// Add flag to session if user is being impersonated
|
||||||
|
session.user.impersonating = true;
|
||||||
|
}
|
||||||
// if (session?.user) {
|
// if (session?.user) {
|
||||||
// session.user.id = user.id; //duplicate
|
// session.user.id = user.id; //duplicate
|
||||||
// }
|
// }
|
||||||
|
@ -27,6 +27,8 @@ export default async function handler(req, res) {
|
|||||||
impersonating: true, // flag to indicate impersonation
|
impersonating: true, // flag to indicate impersonation
|
||||||
originalUser: session.user // save the original user for later
|
originalUser: session.user // save the original user for later
|
||||||
};
|
};
|
||||||
|
// Log the event (simplified example)
|
||||||
|
console.log(`Admin ${session.user} impersonated user ${userToImpersonate.email} on ${new Date().toISOString()}`);
|
||||||
|
|
||||||
// Here you would typically use some method to create a session server-side
|
// Here you would typically use some method to create a session server-side
|
||||||
// For this example, we'll just send the impersonated session as a response
|
// For this example, we'll just send the impersonated session as a response
|
||||||
|
@ -1,12 +1,77 @@
|
|||||||
import path from 'path';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import nc from 'next-connect';
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import multer from 'multer';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
|
||||||
const handler = nc({
|
|
||||||
onError: (err, req, res, next) => {
|
// Generalized Multer configuration
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const subfolder = req.query.subfolder ? decodeURIComponent(req.query.subfolder as string) : 'default';
|
||||||
|
const uploadPath = path.join(process.cwd(), 'public/content', subfolder);
|
||||||
|
|
||||||
|
fs.mkdir(uploadPath, { recursive: true })
|
||||||
|
.then(() => cb(null, uploadPath))
|
||||||
|
.catch(cb);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const filename = decodeURIComponent(file.originalname); // Ensure the filename is correctly decoded
|
||||||
|
const prefix = req.body.prefix ? decodeURIComponent(req.body.prefix) : path.parse(filename).name;
|
||||||
|
cb(null, `${prefix}${path.extname(filename)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileFilter = (req, file, cb) => {
|
||||||
|
// Accept PDFs only
|
||||||
|
if (file.mimetype === 'application/pdf') {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only PDF files are allowed!'), false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upload = multer({ storage, fileFilter });
|
||||||
|
|
||||||
|
const router = createRouter<NextApiRequest, NextApiResponse>();
|
||||||
|
|
||||||
|
router.use(upload.array('file'));
|
||||||
|
|
||||||
|
router.post((req, res) => {
|
||||||
|
console.log(req.files); // Log files to see if PDFs are included
|
||||||
|
if (req.files.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No files were uploaded.' });
|
||||||
|
}
|
||||||
|
// Process uploaded files, assume images are being resized and saved
|
||||||
|
res.json({ message: 'Files uploaded successfully', files: req.files });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(async (req, res) => {
|
||||||
|
// Implement functionality to list files
|
||||||
|
const directory = path.join(process.cwd(), 'public/content', req.query.subfolder as string);
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(directory);
|
||||||
|
const imageUrls = files.map(file => `/content/${req.query.subfolder}/${file}`);
|
||||||
|
res.json({ imageUrls });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Internal Server Error', details: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete(async (req, res) => {
|
||||||
|
// Implement functionality to delete a file
|
||||||
|
const filePath = path.join(process.cwd(), 'public/content', req.query.subfolder as string, req.query.file as string);
|
||||||
|
try {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
res.send('File deleted successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send('Failed to delete the file.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router.handler({
|
||||||
|
onError: (err, req, res) => {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
res.status(500).end('Something broke!');
|
res.status(500).end('Something broke!');
|
||||||
},
|
},
|
||||||
@ -15,131 +80,8 @@ const handler = nc({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
handler.use((req: NextApiRequest, res: NextApiResponse, next) => {
|
|
||||||
const subfolder = req.query.subfolder as string;
|
|
||||||
const upload = createUploadMiddleware(subfolder).array('image');
|
|
||||||
upload(req, res, (err) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).json({ error: 'Failed to upload files.', details: err.message });
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
handler.post((req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
// Process uploaded files
|
|
||||||
// Example response
|
|
||||||
res.json({ message: 'Files uploaded successfully', files: req.files });
|
|
||||||
});
|
|
||||||
|
|
||||||
handler.get((req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
// Handle listing files
|
|
||||||
//listFiles(req, res, req.subfolder);
|
|
||||||
listFiles(req, res, req.query.subfolder as string);
|
|
||||||
});
|
|
||||||
|
|
||||||
handler.delete((req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
// Handle deleting files
|
|
||||||
deleteFile(req, res, req.query.subfolder as string);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
//handling file uploads
|
|
||||||
import multer from 'multer';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
// Generalized Multer configuration
|
|
||||||
export const createUploadMiddleware = (folder: string) => {
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: (req, file, cb) => {
|
|
||||||
const uploadPath = path.join(process.cwd(), 'public/content', folder);
|
|
||||||
if (!fs.existsSync(uploadPath)) {
|
|
||||||
fs.mkdirSync(uploadPath, { recursive: true });
|
|
||||||
}
|
|
||||||
cb(null, uploadPath);
|
|
||||||
},
|
|
||||||
filename: (req, file, cb) => {
|
|
||||||
const prefix = req.body.prefix || path.parse(file.originalname).name;
|
|
||||||
cb(null, `${prefix}${path.extname(file.originalname)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return multer({ storage });
|
|
||||||
};
|
|
||||||
|
|
||||||
async function processFiles(req, res, folder) {
|
|
||||||
if (!req.files || req.files.length === 0) {
|
|
||||||
return res.status(400).json({ error: 'No files uploaded.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadDir = path.join(process.cwd(), 'public/content', folder);
|
|
||||||
const thumbDir = path.join(uploadDir, "thumb");
|
|
||||||
|
|
||||||
if (!fs.existsSync(thumbDir)) {
|
|
||||||
fs.mkdirSync(thumbDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const processedFiles = await Promise.all(req.files.map(async (file) => {
|
|
||||||
const originalPath = path.join(uploadDir, file.filename);
|
|
||||||
const thumbPath = path.join(thumbDir, file.filename);
|
|
||||||
|
|
||||||
await sharp(file.path)
|
|
||||||
.resize({ width: 1920, fit: sharp.fit.inside, withoutEnlargement: true })
|
|
||||||
.jpeg({ quality: 80 })
|
|
||||||
.toFile(originalPath);
|
|
||||||
|
|
||||||
await sharp(file.path)
|
|
||||||
.resize(320, 320, { fit: sharp.fit.inside, withoutEnlargement: true })
|
|
||||||
.toFile(thumbPath);
|
|
||||||
|
|
||||||
fs.unlinkSync(file.path); // Remove temp file
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalUrl: `/content/${folder}/${file.filename}`,
|
|
||||||
thumbUrl: `/content/${folder}/thumb/${file.filename}`
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json(processedFiles);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing files:', error);
|
|
||||||
res.status(500).json({ error: 'Error processing files.' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List files in a directory
|
|
||||||
async function listFiles(req, res, folder) {
|
|
||||||
const directory = path.join(process.cwd(), 'public/content', folder);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await fs.promises.readdir(directory);
|
|
||||||
const imageUrls = files.map(file => `${req.protocol}://${req.get('host')}/content/${folder}/${file}`);
|
|
||||||
res.json({ imageUrls });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error reading uploads directory:', err);
|
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a file
|
|
||||||
async function deleteFile(req, res, folder) {
|
|
||||||
const filename = req.query.file;
|
|
||||||
if (!filename) {
|
|
||||||
return res.status(400).send('Filename is required.');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const filePath = path.join(process.cwd(), 'public/content', folder, filename);
|
|
||||||
await fs.unlink(filePath);
|
|
||||||
res.status(200).send('File deleted successfully.');
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send('Failed to delete the file.');
|
|
||||||
}
|
|
||||||
}
|
|
@ -106,6 +106,7 @@ export default async function handler(req, res) {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
publisherId: userId,
|
publisherId: userId,
|
||||||
|
originalPublisherId: originalPublisher.id,
|
||||||
publicGuid: null, // if this exists, we consider the request open
|
publicGuid: null, // if this exists, we consider the request open
|
||||||
isConfirmed: true
|
isConfirmed: true
|
||||||
}
|
}
|
||||||
@ -161,7 +162,7 @@ export default async function handler(req, res) {
|
|||||||
newPubs: newPubs,
|
newPubs: newPubs,
|
||||||
placeName: assignment.shift.cartEvent.location.name,
|
placeName: assignment.shift.cartEvent.location.name,
|
||||||
dateStr: common.getDateFormated(assignment.shift.startTime),
|
dateStr: common.getDateFormated(assignment.shift.startTime),
|
||||||
time: common.formatTimeHHmm(assignment.shift.startTime),
|
time: common.getTimeFormatted(assignment.shift.startTime),
|
||||||
sentDate: common.getDateFormated(new Date())
|
sentDate: common.getDateFormated(new Date())
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -383,7 +384,7 @@ export default async function handler(req, res) {
|
|||||||
email: pubsToSend[i].email,
|
email: pubsToSend[i].email,
|
||||||
placeName: assignment.shift.cartEvent.location.name,
|
placeName: assignment.shift.cartEvent.location.name,
|
||||||
dateStr: common.getDateFormated(assignment.shift.startTime),
|
dateStr: common.getDateFormated(assignment.shift.startTime),
|
||||||
time: common.formatTimeHHmm(assignment.shift.startTime),
|
time: common.getTimeFormatted(assignment.shift.startTime),
|
||||||
sentDate: common.getDateFormated(new Date())
|
sentDate: common.getDateFormated(new Date())
|
||||||
};
|
};
|
||||||
let results = emailHelper.SendEmailHandlebars(
|
let results = emailHelper.SendEmailHandlebars(
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
import { authOptions } from './auth/[...nextauth]'
|
||||||
|
import { getServerSession } from "next-auth/next"
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { DayOfWeek, AvailabilityType } from '@prisma/client';
|
import { DayOfWeek, AvailabilityType, UserRole, EventLogType } from '@prisma/client';
|
||||||
const common = require('../../src/helpers/common');
|
const common = require('../../src/helpers/common');
|
||||||
const dataHelper = require('../../src/helpers/data');
|
const dataHelper = require('../../src/helpers/data');
|
||||||
const subq = require('../../prisma/bl/subqueries');
|
const subq = require('../../prisma/bl/subqueries');
|
||||||
@ -9,6 +11,7 @@ import { addMinutes } from 'date-fns';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { all } from "axios";
|
import { all } from "axios";
|
||||||
|
import { logger } from "src/helpers/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -46,6 +49,9 @@ export default async function handler(req, res) {
|
|||||||
let monthInfo = common.getMonthDatesInfo(day);
|
let monthInfo = common.getMonthDatesInfo(day);
|
||||||
const searchText = req.query.searchText?.normalize('NFC');
|
const searchText = req.query.searchText?.normalize('NFC');
|
||||||
|
|
||||||
|
const sessionServer = await getServerSession(req, res, authOptions)
|
||||||
|
var isAdmin = sessionServer?.user.role == UserRole.ADMIN
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "initDb":
|
case "initDb":
|
||||||
@ -137,7 +143,7 @@ export default async function handler(req, res) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "getCalendarEvents":
|
case "getCalendarEvents":
|
||||||
let events = await dataHelper.getCalendarEvents(req.query.publisherId, day);
|
let events = await dataHelper.getCalendarEvents(req.query.publisherId, true, true, isAdmin);
|
||||||
res.status(200).json(events);
|
res.status(200).json(events);
|
||||||
|
|
||||||
case "getPublisherInfo":
|
case "getPublisherInfo":
|
||||||
@ -817,10 +823,70 @@ async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
publisherId: newPublisherId,
|
publisherId: newPublisherId,
|
||||||
|
originalPublisherId: oldPublisherId,
|
||||||
isConfirmed: false,
|
isConfirmed: false,
|
||||||
isBySystem: true,
|
isBySystem: true,
|
||||||
isMailSent: false
|
isMailSent: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// log the event
|
||||||
|
let shift = await prisma.shift.findUnique({
|
||||||
|
where: {
|
||||||
|
id: shiftId
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
startTime: true,
|
||||||
|
cartEvent: {
|
||||||
|
select: {
|
||||||
|
location: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
assignments: {
|
||||||
|
include: {
|
||||||
|
publisher: {
|
||||||
|
select: {
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let publishers = await prisma.publisher.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: [oldPublisherId, newPublisherId] }
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let originalPublisher = publishers.find(p => p.id == oldPublisherId);
|
||||||
|
let newPublisher = publishers.find(p => p.id == newPublisherId);
|
||||||
|
let eventLog = await prisma.eventLog.create({
|
||||||
|
data: {
|
||||||
|
date: new Date(),
|
||||||
|
publisher: { connect: { id: oldPublisherId } },
|
||||||
|
shift: { connect: { id: shiftId } },
|
||||||
|
type: EventLogType.AssignmentReplacementManual,
|
||||||
|
content: "Въведено заместване от " + originalPublisher.firstName + " " + originalPublisher.lastName + ". Ще го замества " + newPublisher.firstName + " " + newPublisher.lastName + "."
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("User: " + originalPublisher.email + " replaced his assignment for " + shift.cartEvent.location.name + " " + shift.startTime.toISOString() + " with " + newPublisher.firstName + " " + newPublisher.lastName + "<" + newPublisher.email + ">. EventLogId: " + eventLog.id + "");
|
||||||
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
@ -1,33 +1,139 @@
|
|||||||
|
|
||||||
const webPush = require('web-push')
|
const webPush = require('web-push')
|
||||||
|
|
||||||
|
import common from '../../src/helpers/common';
|
||||||
|
|
||||||
|
//generate and store VAPID keys in .env.local if not already done
|
||||||
|
if (!process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY || !process.env.WEB_PUSH_PRIVATE_KEY) {
|
||||||
|
const { publicKey, privateKey } = webPush.generateVAPIDKeys()
|
||||||
|
console.log('VAPID keys generated:')
|
||||||
|
console.log('Public key:', publicKey)
|
||||||
|
console.log('Private key:', privateKey)
|
||||||
|
console.log('Store these keys in your .env.local file:')
|
||||||
|
console.log('NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=', publicKey)
|
||||||
|
console.log('WEB_PUSH_PRIVATE_KEY=', privateKey)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
webPush.setVapidDetails(
|
webPush.setVapidDetails(
|
||||||
`mailto:${process.env.WEB_PUSH_EMAIL}`,
|
`mailto:${process.env.WEB_PUSH_EMAIL}`,
|
||||||
process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY,
|
process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY,
|
||||||
process.env.WEB_PUSH_PRIVATE_KEY
|
process.env.WEB_PUSH_PRIVATE_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
const Notification = (req, res) => {
|
const Notification = async (req, res) => {
|
||||||
if (req.method == 'POST') {
|
if (req.method == 'GET') {
|
||||||
const { subscription } = req.body
|
res.statusCode = 200
|
||||||
|
res.setHeader('Allow', 'POST')
|
||||||
|
let subs = 0
|
||||||
|
if (req.query && req.query.id) {
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
const publisher = await prisma.publisher.findUnique({
|
||||||
|
where: { id: req.query.id },
|
||||||
|
select: { pushSubscription: true }
|
||||||
|
});
|
||||||
|
subs = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription.length : (publisher.pushSubscription ? 1 : 0);
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// send the public key in the response headers
|
||||||
|
//res.setHeader('Content-Type', 'text/plain')
|
||||||
|
res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs })
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
if (req.method == 'PUT') {
|
||||||
|
// store the subscription object in the database
|
||||||
|
// publisher.pushSubscription = subscription
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
const { subscription, id } = req.body
|
||||||
|
const publisher = await prisma.publisher.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { pushSubscription: true }
|
||||||
|
});
|
||||||
|
|
||||||
webPush
|
let subscriptions = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription : (publisher.pushSubscription ? [publisher.pushSubscription] : []);
|
||||||
.sendNotification(
|
const index = subscriptions.findIndex(sub => sub.endpoint === subscription.endpoint);
|
||||||
subscription,
|
|
||||||
JSON.stringify({ title: 'Hello Web Push', message: 'Your web push notification is here!' })
|
if (index !== -1) {
|
||||||
)
|
subscriptions[index] = subscription; // Update existing subscription
|
||||||
.then(response => {
|
} else {
|
||||||
res.writeHead(response.statusCode, response.headers).end(response.body)
|
subscriptions.push(subscription); // Add new subscription
|
||||||
})
|
}
|
||||||
.catch(err => {
|
|
||||||
if ('statusCode' in err) {
|
await prisma.publisher.update({
|
||||||
res.writeHead(err.statusCode, err.headers).end(err.body)
|
where: { id },
|
||||||
} else {
|
data: { pushSubscription: subscriptions }
|
||||||
console.error(err)
|
});
|
||||||
res.statusCode = 500
|
console.log('Subscription for publisher', id, 'updated:', subscription)
|
||||||
res.end()
|
res.send({ subs: subscriptions.length })
|
||||||
}
|
res.statusCode = 200
|
||||||
})
|
res.end()
|
||||||
|
}
|
||||||
|
if (req.method == 'DELETE') {
|
||||||
|
// remove the subscription object from the database
|
||||||
|
// publisher.pushSubscription = null
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
const { subscriptionId, id } = req.body;
|
||||||
|
|
||||||
|
const publisher = await prisma.publisher.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { pushSubscription: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
let subscriptions = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription : (publisher.pushSubscription ? [publisher.pushSubscription] : []);
|
||||||
|
try {
|
||||||
|
subscriptions = subscriptionId ? subscriptions.filter(sub => sub.endpoint !== subscriptionId) : [];
|
||||||
|
await prisma.publisher.update({
|
||||||
|
where: { id },
|
||||||
|
data: { pushSubscription: subscriptions }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
await prisma.publisher.update({
|
||||||
|
where: { id },
|
||||||
|
data: { pushSubscription: null }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Subscription for publisher', id, 'deleted')
|
||||||
|
res.send({ subs: subscriptions.length })
|
||||||
|
res.statusCode = 200
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (req.method == 'POST') {//title = "ССС", message = "Ще получите уведомление по този начин.")
|
||||||
|
const { subscription, id, broadcast, title = 'ССОМ', message = 'Ще получавате уведомления така.', actions } = req.body
|
||||||
|
if (broadcast) {
|
||||||
|
await broadcastPush(title, message, actions)
|
||||||
|
res.statusCode = 200
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else if (id) {
|
||||||
|
await sendPush(id, title, message.actions)
|
||||||
|
res.statusCode = 200
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
} else if (subscription) {
|
||||||
|
await webPush
|
||||||
|
.sendNotification(
|
||||||
|
subscription,
|
||||||
|
JSON.stringify({ title, message, actions })
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
res.writeHead(response.statusCode, response.headers).end(response.body)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if ('statusCode' in err) {
|
||||||
|
res.writeHead(err.statusCode, err.headers).end(err.body)
|
||||||
|
} else {
|
||||||
|
console.error(err)
|
||||||
|
res.statusCode = 500
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.statusCode = 405
|
res.statusCode = 405
|
||||||
res.end()
|
res.end()
|
||||||
@ -35,3 +141,54 @@ const Notification = (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default Notification
|
export default Notification
|
||||||
|
|
||||||
|
//export pushNotification(userId or email) for use in other files
|
||||||
|
export const sendPush = async (id, title, message, actions) => {
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
const publisher = await prisma.publisher.findUnique({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
if (!publisher.pushSubscription) {
|
||||||
|
console.log('No push subscription found for publisher', id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await webPush
|
||||||
|
.sendNotification(
|
||||||
|
publisher.pushSubscription,
|
||||||
|
JSON.stringify({ title, message, actions })
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
console.log('Push notification sent to publisher', id)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error sending push notification to publisher', id, ':', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//export breoadcastNotification for use in other files
|
||||||
|
export const broadcastPush = async (title, message, actions) => {
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
const publishers = await prisma.publisher.findMany({
|
||||||
|
where: { pushSubscription: { not: null } }
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const publisher of publishers) {
|
||||||
|
if (Array.isArray(publisher.pushSubscription) && publisher.pushSubscription.length) {
|
||||||
|
for (const subscription of publisher.pushSubscription) {
|
||||||
|
await webPush.sendNotification(
|
||||||
|
subscription, // Here subscription is each individual subscription object
|
||||||
|
JSON.stringify({ title, message, actions })
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
console.log('Push notification sent to device', subscription.endpoint, 'of publisher', publisher.id);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error sending push notification to device', subscription.endpoint, 'of publisher', publisher.id, ':', err);
|
||||||
|
// Optionally handle failed subscriptions, e.g., remove outdated or invalid subscriptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No valid subscriptions found for publisher', publisher.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -58,10 +58,7 @@ export default function SignIn({ csrfToken }) {
|
|||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="signin">
|
<div className="signin">
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
|
||||||
{/* Page Title */}
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mt-6">Вход</h1>
|
<h1 className="text-2xl font-bold text-gray-900 mt-6">Вход</h1>
|
||||||
|
|
||||||
{/* Section for Social Sign-On Providers */}
|
|
||||||
<div className="mt-8 w-full max-w-md px-4 py-8 bg-white shadow rounded-lg">
|
<div className="mt-8 w-full max-w-md px-4 py-8 bg-white shadow rounded-lg">
|
||||||
{/* <h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Sign in with a Social Media Account</h2> */}
|
{/* <h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Sign in with a Social Media Account</h2> */}
|
||||||
<button onClick={() => signIn('google', { callbackUrl: '/' })}
|
<button onClick={() => signIn('google', { callbackUrl: '/' })}
|
||||||
@ -70,22 +67,24 @@ export default function SignIn({ csrfToken }) {
|
|||||||
src="https://authjs.dev/img/providers/google.svg" className="mr-2" />
|
src="https://authjs.dev/img/providers/google.svg" className="mr-2" />
|
||||||
Влез чрез Google
|
Влез чрез Google
|
||||||
</button>
|
</button>
|
||||||
{/* Add more buttons for other SSO providers here in similar style */}
|
{/* Apple Sign-In Button */}
|
||||||
|
{/* <button onClick={() => signIn('apple', { callbackUrl: '/' })}
|
||||||
|
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||||
|
<img loading="lazy" height="24" width="24" alt="Apple logo"
|
||||||
|
src="https://authjs.dev/img/providers/apple.svg" className="mr-2" />
|
||||||
|
Влез чрез Apple
|
||||||
|
</button> */}
|
||||||
|
{/* microsoft */}
|
||||||
|
{/* <button onClick={() => signIn('azure-ad', { callbackUrl: '/' })}
|
||||||
|
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||||
|
<img loading="lazy" height="24" width="24" alt="Microsoft logo"
|
||||||
|
src="https://authjs.dev/img/providers/azure-ad.svg" className="mr-2" />
|
||||||
|
Влез чрез Microsoft
|
||||||
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
{/* Apple Sign-In Button */}
|
|
||||||
<button onClick={() => signIn('apple', { callbackUrl: '/' })}
|
|
||||||
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
|
||||||
<img loading="lazy" height="24" width="24" alt="Apple logo"
|
|
||||||
src="https://authjs.dev/img/providers/apple.svg" className="mr-2" />
|
|
||||||
Влез чрез Apple
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Divider (Optional) */}
|
|
||||||
<div className="w-full max-w-xs mt-8 mb-8">
|
<div className="w-full max-w-xs mt-8 mb-8">
|
||||||
<hr className="border-t border-gray-300" />
|
<hr className="border-t border-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Local Account Email and Password Form */}
|
|
||||||
<div className="w-full max-w-md mt-8 mb-8 px-4 py-8 bg-white shadow rounded-lg">
|
<div className="w-full max-w-md mt-8 mb-8 px-4 py-8 bg-white shadow rounded-lg">
|
||||||
<h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Влез с локален акаунт</h2>
|
<h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Влез с локален акаунт</h2>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@ -131,9 +130,11 @@ export default function SignIn({ csrfToken }) {
|
|||||||
|
|
||||||
// This gets called on every request
|
// This gets called on every request
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
|
const csrfToken = await getCsrfToken(context);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
csrfToken: await getCsrfToken(context),
|
...(csrfToken ? { csrfToken } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,9 @@ 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 LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
|
// import notify api
|
||||||
|
import { sendPush, broadcastPush } from '../../api/notify';
|
||||||
|
const { DateTime } = require('luxon');
|
||||||
|
|
||||||
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
|
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
|
||||||
|
|
||||||
@ -544,7 +547,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
var dayName = common.DaysOfWeekArray[value.getDayEuropean()];
|
var dayName = common.DaysOfWeekArray[value.getDayEuropean()];
|
||||||
const cartEvent = events.find(event => event.dayofweek == dayName);
|
const cartEvent = events.find(event => event.dayofweek == dayName);
|
||||||
lastShift = {
|
lastShift = {
|
||||||
endTime: new Date(value.setHours(9, 0, 0, 0)),
|
endTime: DateTime.fromJSDate(value).setZone('Europe/Sofia', { keepLocalTime: true }).set({ hour: 9 }).toJSDate(),
|
||||||
cartEventId: cartEvent.id
|
cartEventId: cartEvent.id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -733,7 +736,19 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
|
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
|
||||||
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
|
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
|
||||||
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
||||||
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
<button tooltip="желани участия на месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
||||||
|
<button tooltip="push" title="push" className={`badge py-1 px-2 rounded-md text-xs bg-red-100`}
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch('/api/notify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ broadcast: true, message: "Тестово съобщение", title: "Това е тестово съобщение от https://sofia.mwitnessing.com" })
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>+</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import Layout from "../../../components/layout";
|
|||||||
import LocationCard from "../../../components/location/LocationCard";
|
import LocationCard from "../../../components/location/LocationCard";
|
||||||
import axiosServer from '../../../src/axiosServer';
|
import axiosServer from '../../../src/axiosServer';
|
||||||
import ProtectedRoute from '../../../components/protectedRoute';
|
import ProtectedRoute from '../../../components/protectedRoute';
|
||||||
|
import CongregationCRUD from "../publishers/congregationCRUD";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
item: Location;
|
item: Location;
|
||||||
}
|
}
|
||||||
@ -32,6 +33,7 @@ function LocationsPage({ items = [] }: IProps) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
<CongregationCRUD />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
103
pages/cart/publishers/congregationCRUD.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// a simple CRUD componenet for congregations for admins
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import axiosInstance from '../../../src/axiosSecure';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import Layout from '../../../components/layout';
|
||||||
|
import ProtectedRoute from '../../../components/protectedRoute';
|
||||||
|
import { UserRole } from '@prisma/client';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
export default function CongregationCRUD() {
|
||||||
|
const [congregations, setCongregations] = useState([]);
|
||||||
|
const [newCongregation, setNewCongregation] = useState('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const fetchCongregations = async () => {
|
||||||
|
try {
|
||||||
|
const { data: congregationsData } = await axiosInstance.get(`/api/data/congregations`);
|
||||||
|
setCongregations(congregationsData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCongregation = async () => {
|
||||||
|
try {
|
||||||
|
await axiosInstance.post(`/api/data/congregations`, { name: newCongregation, address: "" });
|
||||||
|
toast.success('Успешно добавен сбор');
|
||||||
|
setNewCongregation('');
|
||||||
|
fetchCongregations();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCongregation = async (id) => {
|
||||||
|
try {
|
||||||
|
await axiosInstance.delete(`/api/data/congregations/${id}`);
|
||||||
|
toast.success('Успешно изтрит сбор');
|
||||||
|
fetchCongregations();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCongregations();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]}>
|
||||||
|
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
||||||
|
<div className="flex flex-col w-full px-4">
|
||||||
|
<h1 className="text-2xl font-bold text-center">Сборове</h1>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newCongregation}
|
||||||
|
onChange={(e) => setNewCongregation(e.target.value)}
|
||||||
|
placeholder="Име на сбор"
|
||||||
|
className="px-4 py-2 rounded-md border border-gray-300"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addCongregation}
|
||||||
|
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
>
|
||||||
|
Добави
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Име</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{congregations.map((congregation) => (
|
||||||
|
<tr key={congregation.id}>
|
||||||
|
<td>{congregation.name}</td>
|
||||||
|
<td className='right'>
|
||||||
|
{/* <button
|
||||||
|
onClick={() => router.push(`/cart/publishers/congregation/${congregation.id}`)}
|
||||||
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
>
|
||||||
|
Преглед
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
onClick={() => deleteCongregation(congregation.id)}
|
||||||
|
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
>
|
||||||
|
Изтрий
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ProtectedRoute>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -64,13 +64,23 @@ export const getServerSideProps = async (context) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!item) {
|
if (!item) {
|
||||||
const user = context.req.session.user;
|
const user = context.req.session?.user;
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
// redirect to '/auth/signin'. assure it is not relative path
|
||||||
|
redirect: {
|
||||||
|
destination: process.env.NEXT_PUBLIC_PUBLIC_URL + "/auth/signin",
|
||||||
|
permanent: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const message = encodeURIComponent(`Този имейл (${user?.email}) не е регистриран. Моля свържете се с администратора.`);
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.',
|
destination: `/message?message=${message}`,
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
|
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import * as XLSX from "xlsx";
|
|||||||
// import { Table } from "react-bootstrap";
|
// import { Table } from "react-bootstrap";
|
||||||
import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external";
|
import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external";
|
||||||
|
|
||||||
import moment from 'moment';
|
|
||||||
// import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ???
|
// import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ???
|
||||||
|
|
||||||
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
|
@ -60,7 +60,7 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
<div className="container ">
|
<div className="container ">
|
||||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
<h1 className="text-xl sm: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.length > 0 ? (assignments.map((assignment) => (
|
||||||
<div key={assignment.dateStr + assignments.indexOf(assignment)} 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>
|
||||||
@ -117,7 +117,13 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))) :
|
||||||
|
<div className="bg-white shadow overflow-hidden rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:px-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">За сега нямате бъдещи назначени смени. Можете да проверите дали вашите възножности са актуални.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal isOpen={isModalOpen}
|
<Modal isOpen={isModalOpen}
|
||||||
@ -168,10 +174,12 @@ export const getServerSideProps = async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prisma = common.getPrismaClient();
|
const prisma = common.getPrismaClient();
|
||||||
const monthInfo = common.getMonthInfo(new Date());
|
let today = new Date();
|
||||||
//minus 1 day from the firstMonday to get the last Sunday
|
today.setHours(0, 0, 0, 0);
|
||||||
const lastSunday = new Date(monthInfo.firstMonday);
|
// const monthInfo = common.getMonthInfo(today);
|
||||||
lastSunday.setDate(lastSunday.getDate() - 1);
|
// //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({
|
const publisher = await prisma.publisher.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
@ -179,7 +187,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
some: {
|
some: {
|
||||||
shift: {
|
shift: {
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: lastSunday,
|
gte: today,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -208,7 +216,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignments = publisher?.assignments.filter(a => a.shift.startTime >= lastSunday && a.shift.isPublished) || [];
|
const assignments = publisher?.assignments.filter(a => a.shift.startTime >= today && a.shift.isPublished) || [];
|
||||||
|
|
||||||
|
|
||||||
const transformedAssignments = assignments?.sort((a, b) => a.shift.startTime - b.shift.startTime)
|
const transformedAssignments = assignments?.sort((a, b) => a.shift.startTime - b.shift.startTime)
|
||||||
|
@ -99,7 +99,7 @@ function ContactsPage({ allPublishers }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="text-xl font-semibold mb-4">Статистика </h1>
|
<h1 className="text-xl font-semibold mb-4">Статистика </h1>
|
||||||
<h5 className="text-lg font-semibold mb-4">{pubWithAssignmentsCount} участника с предпочитания за месеца (от {filteredPublishers.length} )</h5>
|
<h5 className="text-lg font-semibold mb-4">{pubWithAssignmentsCount} участника с предпочитания за месеца (от {filteredPublishers.length} )</h5>
|
||||||
|
@ -42,7 +42,7 @@ export default function EventLogList() {
|
|||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||||
|
|
||||||
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
||||||
<div className="flex flex-col w-full px-4">
|
<div className="flex flex-col w-full px-4">
|
||||||
|
@ -9,7 +9,7 @@ import ProtectedRoute from '../../../components/protectedRoute';
|
|||||||
function NewPage(loc: Location) {
|
function NewPage(loc: Location) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||||
<div className="h-5/6 grid place-items-center">
|
<div className="h-5/6 grid place-items-center">
|
||||||
<ExperienceForm />
|
<ExperienceForm />
|
||||||
</div></ProtectedRoute>
|
</div></ProtectedRoute>
|
||||||
|
@ -83,7 +83,7 @@ export default function Reports() {
|
|||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||||
|
|
||||||
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
||||||
<div className="flex flex-col w-full px-4">
|
<div className="flex flex-col w-full px-4">
|
||||||
|
@ -9,7 +9,7 @@ import ProtectedRoute from '../../../components/protectedRoute';
|
|||||||
function NewPage(loc: Location) {
|
function NewPage(loc: Location) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||||
<div className="h-5/6 grid place-items-center">
|
<div className="h-5/6 grid place-items-center">
|
||||||
<ReportForm />
|
<ReportForm />
|
||||||
</div></ProtectedRoute>
|
</div></ProtectedRoute>
|
||||||
|
@ -15,13 +15,16 @@ import { getServerSession } from "next-auth/next"
|
|||||||
|
|
||||||
import PublisherSearchBox from '../components/publisher/PublisherSearchBox';
|
import PublisherSearchBox from '../components/publisher/PublisherSearchBox';
|
||||||
import PublisherInlineForm from '../components/publisher/PublisherInlineForm';
|
import PublisherInlineForm from '../components/publisher/PublisherInlineForm';
|
||||||
|
import CartEventForm from "components/cartevent/CartEventForm";
|
||||||
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
initialItems: Availability[];
|
initialItems: Availability[];
|
||||||
initialUserId: string;
|
initialUserId: string;
|
||||||
|
cartEvents: any;
|
||||||
|
lastPublishedDate: Date;
|
||||||
}
|
}
|
||||||
export default function IndexPage({ initialItems, initialUserId }: IProps) {
|
export default function IndexPage({ initialItems, initialUserId, cartEvents, lastPublishedDate }: IProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [userName, setUserName] = useState(session?.user?.name);
|
const [userName, setUserName] = useState(session?.user?.name);
|
||||||
const [userId, setUserId] = useState(initialUserId);
|
const [userId, setUserId] = useState(initialUserId);
|
||||||
@ -68,7 +71,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute deniedMessage="">
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER, UserRole.EXTERNAL]} deniedMessage="">
|
||||||
<h1 className="pt-2 pb-1 text-xl font-bold text-center">Графика на {userName}</h1>
|
<h1 className="pt-2 pb-1 text-xl font-bold text-center">Графика на {userName}</h1>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
||||||
<PublisherSearchBox selectedId={userId} infoText="" onChange={handleUserSelection} />
|
<PublisherSearchBox selectedId={userId} infoText="" onChange={handleUserSelection} />
|
||||||
@ -78,7 +81,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
|
|||||||
<div className="text-center font-bold pb-3 xs:pb-1">
|
<div className="text-center font-bold pb-3 xs:pb-1">
|
||||||
<PublisherInlineForm publisherId={userId} />
|
<PublisherInlineForm publisherId={userId} />
|
||||||
</div>
|
</div>
|
||||||
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} />
|
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} cartEvents={cartEvents} lastPublishedDate={lastPublishedDate} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
@ -119,7 +122,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
|
|||||||
// ...item,
|
// ...item,
|
||||||
// startTime: item.startTime.toISOString(),
|
// startTime: item.startTime.toISOString(),
|
||||||
// endTime: item.endTime.toISOString(),
|
// endTime: item.endTime.toISOString(),
|
||||||
// name: common.getTimeFomatted(item.startTime) + "-" + common.getTimeFomatted(item.endTime),
|
// name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime),
|
||||||
// //endDate can be null
|
// //endDate can be null
|
||||||
// endDate: item.endDate ? item.endDate.toISOString() : null,
|
// endDate: item.endDate ? item.endDate.toISOString() : null,
|
||||||
// type: 'availability',
|
// type: 'availability',
|
||||||
@ -175,7 +178,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
|
|||||||
// endTime: item.shift.endTime.toISOString(),
|
// endTime: item.shift.endTime.toISOString(),
|
||||||
// // name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "),
|
// // name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "),
|
||||||
// //name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "),
|
// //name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "),
|
||||||
// name: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
|
// name: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)),
|
||||||
// type: 'assignment',
|
// type: 'assignment',
|
||||||
// //delete shift object
|
// //delete shift object
|
||||||
// shift: null,
|
// shift: null,
|
||||||
@ -193,29 +196,84 @@ export const getServerSideProps = async (context) => {
|
|||||||
req: context.req,
|
req: context.req,
|
||||||
allowedRoles: [/* ...allowed roles... */]
|
allowedRoles: [/* ...allowed roles... */]
|
||||||
});
|
});
|
||||||
const session = await getSession(context);
|
// const session = await getSession(context);
|
||||||
const sessionServer = await getServerSession(context.req, context.res, authOptions)
|
const sessionServer = await getServerSession(context.req, context.res, authOptions)
|
||||||
|
|
||||||
if (!session) { return { props: {} } }
|
if (!sessionServer) {
|
||||||
const role = session?.user.role;
|
return {
|
||||||
console.log("server role: " + role);
|
redirect: {
|
||||||
const userId = session?.user.id;
|
destination: '/auth/signin',
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var items = await dataHelper.getCalendarEvents(session.user.id);
|
const role = sessionServer?.user.role;
|
||||||
|
console.log("server role: " + role);
|
||||||
|
const userId = sessionServer?.user.id;
|
||||||
|
var isAdmin = sessionServer?.user.role == UserRole.ADMIN;//role.localeCompare(UserRole.ADMIN) === 0;
|
||||||
|
|
||||||
|
var items = await dataHelper.getCalendarEvents(userId, true, true, isAdmin);
|
||||||
// common.convertDatesToISOStrings(items);
|
// common.convertDatesToISOStrings(items);
|
||||||
//serializable dates
|
//serializable dates
|
||||||
items = items.map(item => ({
|
items = items.map(item => {
|
||||||
...item,
|
const updatedItem = {
|
||||||
startTime: item.startTime.toISOString(),
|
...item,
|
||||||
endTime: item.endTime.toISOString(),
|
startTime: item.startTime.toISOString(),
|
||||||
date: item.date.toISOString(),
|
endTime: item.endTime.toISOString(),
|
||||||
}));
|
date: item.date.toISOString(),
|
||||||
|
name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updatedItem.shift) {
|
||||||
|
updatedItem.shift = {
|
||||||
|
...updatedItem.shift,
|
||||||
|
startTime: updatedItem.shift.startTime.toISOString(),
|
||||||
|
endTime: updatedItem.shift.endTime.toISOString()
|
||||||
|
};
|
||||||
|
updatedItem.isPublished = updatedItem.shift.isPublished;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
// log first availability startTime to verify timezone and UTC conversion
|
||||||
|
|
||||||
|
console.log("First availability startTime: " + items[0]?.startTime);
|
||||||
|
console.log("First availability startTime: " + items[0]?.startTime.toLocaleString());
|
||||||
|
|
||||||
|
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
|
let cartEvents = await prisma.cartEvent.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
dayofweek: true,
|
||||||
|
shiftDuration: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cartEvents = common.convertDatesToISOStrings(cartEvents);
|
||||||
|
const lastPublishedDate = (await prisma.shift.findFirst({
|
||||||
|
where: {
|
||||||
|
isPublished: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
endTime: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
endTime: 'desc'
|
||||||
|
}
|
||||||
|
})).endTime;
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
initialItems: items,
|
initialItems: items,
|
||||||
userId: session?.user.id,
|
userId: sessionServer?.user.id,
|
||||||
|
cartEvents: cartEvents,
|
||||||
|
lastPublishedDate: lastPublishedDate.toISOString(),
|
||||||
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
|
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import path from 'path';
|
|||||||
import { url } from 'inspector';
|
import { url } from 'inspector';
|
||||||
import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute";
|
import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute";
|
||||||
import axiosInstance from '../src/axiosSecure';
|
import axiosInstance from '../src/axiosSecure';
|
||||||
|
import { UserRole } from "@prisma/client";
|
||||||
|
|
||||||
|
|
||||||
const PDFViewerPage = ({ pdfFiles }) => {
|
const PDFViewerPage = ({ pdfFiles }) => {
|
||||||
@ -22,17 +23,23 @@ const PDFViewerPage = ({ pdfFiles }) => {
|
|||||||
|
|
||||||
const handleFileUpload = async (event) => {
|
const handleFileUpload = async (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
//utf-8 encoding
|
||||||
|
// const formData = new FormData();
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
// formData.append('file', file);
|
||||||
|
const newFile = new File([file], encodeURI(file.name), { type: file.type });
|
||||||
|
formData.append('file', newFile);
|
||||||
|
|
||||||
const subfolder = 'permits'; // Change this as needed based on your subfolder structure
|
const subfolder = 'permits'; // Change this as needed based on your subfolder structure
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.post(`/api/content/${subfolder}`, formData, {
|
const response = await axiosInstance.post(`/api/content/${subfolder}`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data',
|
||||||
|
// 'Content-Encoding': 'utf-8'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setFiles([...files, response.data]);
|
const newFiles = response.data.files.map(file => ({ name: decodeURIComponent(file.originalname), url: file.path }));
|
||||||
|
setFiles([...files, ...newFiles]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading file:', error);
|
console.error('Error uploading file:', error);
|
||||||
}
|
}
|
||||||
@ -42,18 +49,36 @@ const PDFViewerPage = ({ pdfFiles }) => {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
|
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
|
||||||
<ProtectedRoute>
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
||||||
<input type="file" onChange={handleFileUpload} className="mb-4" />
|
<div className="border border-blue-500 p-4 rounded shadow-md">
|
||||||
{files.map((file, index) => (
|
<div className="mb-6">
|
||||||
<div key={file.name} className="py-2">
|
<p className="text-lg mb-2">За да качите файл кликнете на бутона по-долу и изберете файл от вашия компютър.</p>
|
||||||
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
<input type="file" onChange={handleFileUpload} className="block w-full text-sm text-gray-600
|
||||||
{file.name}
|
file:mr-4 file:py-2 file:px-4
|
||||||
</a>
|
file:border-0
|
||||||
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">
|
file:text-sm file:font-semibold
|
||||||
изтрий
|
file:bg-blue-500 file:text-white
|
||||||
</button>
|
hover:file:bg-blue-600"/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Съществуващи файлове:</h3>
|
||||||
|
{files.length > 0 ? (
|
||||||
|
files.map((file, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between mb-2 p-2 hover:bg-blue-50 rounded">
|
||||||
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target="_blank" rel="noopener noreferrer">
|
||||||
|
{file.name}
|
||||||
|
</a>
|
||||||
|
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
||||||
|
изтрий
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">Няма качени файлове.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
||||||
<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 */}
|
||||||
@ -102,5 +127,4 @@ export const getServerSideProps = async (context) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// export const getServerSideProps = async (context) => {
|
|
||||||
|
|
||||||
|
36
prisma/administrative_scripts/fix_availability_time_DHT
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
UPDATE `Availability`
|
||||||
|
SET
|
||||||
|
`startTime` = ADDTIME(`startTime`, '01:00:00'),
|
||||||
|
`endTime` = ADDTIME(`endTime`, '01:00:00'),
|
||||||
|
`name` = CONCAT(`name`, ' (DHT)')
|
||||||
|
WHERE
|
||||||
|
`startTime` LIKE '%05:00%' -- this is 9:00; -4 hours difference, where -3 is expected
|
||||||
|
OR `startTime` LIKE '%06:30%' -- this is 10:30; -4 hours difference, where -3 is expected
|
||||||
|
OR `startTime` LIKE '%08:00%' -- this is 12:00; -4 hours difference, where -3 is expected
|
||||||
|
OR `startTime` LIKE '%09:30%' -- this is 13:30 UTC
|
||||||
|
OR `startTime` LIKE '%11:00%' -- this is 15:00 UTC
|
||||||
|
OR `startTime` LIKE '%12:30%' -- this is 16:30 UTC
|
||||||
|
OR `startTime` LIKE '%14:00%' -- this is 18:00 UTC
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.firstName,
|
||||||
|
p.lastName,
|
||||||
|
a.id,
|
||||||
|
name,
|
||||||
|
dayofweek,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
dayOfMonth,
|
||||||
|
weekOfMonth,
|
||||||
|
isFromPreviousAssignment,
|
||||||
|
isFromPreviousMonth,
|
||||||
|
endDate,
|
||||||
|
repeatWeekly,
|
||||||
|
dateOfEntry,
|
||||||
|
parentAvailabilityId
|
||||||
|
FROM
|
||||||
|
`Availability` a
|
||||||
|
left join Publisher p on p.id = publisherId
|
||||||
|
WHERE
|
||||||
|
name LIKE '%(DHT)%'
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Publisher` ADD COLUMN `pushSubscription` JSON NULL;
|
@ -0,0 +1,33 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Assignment`
|
||||||
|
ADD COLUMN `originalPublisherId` VARCHAR(191) NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Message` ADD COLUMN `publicUntil` DATETIME(3) NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Publisher`
|
||||||
|
ADD COLUMN `congregationId` INTEGER NULL,
|
||||||
|
ADD COLUMN `locale` VARCHAR(191) NULL DEFAULT 'bg';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Report` ADD COLUMN `comments` VARCHAR(191) NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Congregation` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`address` VARCHAR(191) NOT NULL,
|
||||||
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Publisher`
|
||||||
|
ADD CONSTRAINT `Publisher_congregationId_fkey` FOREIGN KEY (`congregationId`) REFERENCES `Congregation` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Assignment`
|
||||||
|
ADD CONSTRAINT `Assignment_originalPublisherId_fkey` FOREIGN KEY (`originalPublisherId`) REFERENCES `Publisher` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,19 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `eventlog`
|
||||||
|
MODIFY `type` ENUM(
|
||||||
|
'AssignmentReplacementManual', 'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
|
||||||
|
) NOT NULL;
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
`Congregation`
|
||||||
|
VALUES (1, 'Перник', '', 1),
|
||||||
|
(2, 'София Люлин', '', 1),
|
||||||
|
(3, 'София Юг', '', 1),
|
||||||
|
(4, 'София Надежда', '', 1),
|
||||||
|
(5, 'София Руски', '', 1),
|
||||||
|
(6, 'София Факултета', '', 1),
|
||||||
|
(7, 'София Изток', '', 1),
|
||||||
|
(8, 'София Младост', '', 1),
|
||||||
|
(9, 'София Английски', '', 1),
|
||||||
|
(10, 'Ботевград', '', 1),
|
||||||
|
(11, 'София Дружба', '', 1);
|
@ -123,6 +123,19 @@ model Publisher {
|
|||||||
Message Message[]
|
Message Message[]
|
||||||
EventLog EventLog[]
|
EventLog EventLog[]
|
||||||
lastLogin DateTime?
|
lastLogin DateTime?
|
||||||
|
pushSubscription Json?
|
||||||
|
originalAssignments Assignment[] @relation("OriginalPublisher")
|
||||||
|
congregation Congregation? @relation(fields: [congregationId], references: [id])
|
||||||
|
congregationId Int?
|
||||||
|
locale String? @default("bg")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Congregation {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
address String
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
publishers Publisher[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Availability {
|
model Availability {
|
||||||
@ -180,23 +193,25 @@ model Shift {
|
|||||||
//date DateTime
|
//date DateTime
|
||||||
reportId Int? @unique
|
reportId Int? @unique
|
||||||
Report Report? @relation(fields: [reportId], references: [id])
|
Report Report? @relation(fields: [reportId], references: [id])
|
||||||
isPublished Boolean @default(false) //NEW v1.0.1
|
isPublished Boolean @default(false)
|
||||||
EventLog EventLog[]
|
EventLog EventLog[]
|
||||||
|
|
||||||
@@map("Shift")
|
@@map("Shift")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Assignment {
|
model Assignment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
||||||
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
|
||||||
isBySystem 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)
|
||||||
publicGuid String? @unique
|
publicGuid String? @unique
|
||||||
|
originalPublisherId String? // New field to store the original publisher id when the assignment is replaced
|
||||||
|
originalPublisher Publisher? @relation("OriginalPublisher", fields: [originalPublisherId], references: [id])
|
||||||
|
|
||||||
@@map("Assignment")
|
@@map("Assignment")
|
||||||
}
|
}
|
||||||
@ -236,6 +251,7 @@ model Report {
|
|||||||
|
|
||||||
experienceInfo String? @db.LongText
|
experienceInfo String? @db.LongText
|
||||||
type ReportType @default(ServiceReport)
|
type ReportType @default(ServiceReport)
|
||||||
|
comments String?
|
||||||
|
|
||||||
@@map("Report")
|
@@map("Report")
|
||||||
}
|
}
|
||||||
@ -257,9 +273,11 @@ model Message {
|
|||||||
isRead Boolean @default(false)
|
isRead Boolean @default(false)
|
||||||
isPublic Boolean @default(false)
|
isPublic Boolean @default(false)
|
||||||
type MessageType @default(Email)
|
type MessageType @default(Email)
|
||||||
|
publicUntil DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventLogType {
|
enum EventLogType {
|
||||||
|
AssignmentReplacementManual
|
||||||
AssignmentReplacementRequested
|
AssignmentReplacementRequested
|
||||||
AssignmentReplacementAccepted
|
AssignmentReplacementAccepted
|
||||||
SentEmail
|
SentEmail
|
||||||
|
Before Width: | Height: | Size: 25 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 22 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 17 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 1.5 MiB |
@ -2,6 +2,16 @@
|
|||||||
"theme_color": "#ffffff",
|
"theme_color": "#ffffff",
|
||||||
"background_color": "#e36600",
|
"background_color": "#e36600",
|
||||||
"icons": [
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "pwa-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "pwa-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"purpose": "maskable",
|
"purpose": "maskable",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
|
23
server.js
@ -32,8 +32,10 @@ const PROTOCOL = process.env.PROTOCOL;
|
|||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const HOST = process.env.HOST;
|
const HOST = process.env.HOST;
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
// production and test environments run with optimized build
|
||||||
|
const dev = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
|
||||||
const nextApp = next({ dev });
|
const nextApp = next({ dev });
|
||||||
|
|
||||||
const nextHandler = nextApp.getRequestHandler();
|
const nextHandler = nextApp.getRequestHandler();
|
||||||
console.log("process.env.PROTOCOL = ", process.env.PROTOCOL);
|
console.log("process.env.PROTOCOL = ", process.env.PROTOCOL);
|
||||||
process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_PUBLIC_URL; //NEXTAUTH_URL mandatory for next-auth
|
process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_PUBLIC_URL; //NEXTAUTH_URL mandatory for next-auth
|
||||||
@ -41,13 +43,17 @@ console.log("process.env.NEXT_PUBLIC_PUBLIC_URL = ", process.env.NEXT_PUBLIC_PUB
|
|||||||
console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
|
console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
|
||||||
console.log("process.env.PORT = ", process.env.PORT);
|
console.log("process.env.PORT = ", process.env.PORT);
|
||||||
console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT);
|
console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT);
|
||||||
console.log("process.env.DATABASE_URL = ", process.env.DATABASE_URL);
|
|
||||||
console.log("process.env.DATABASE = ", process.env.DATABASE);
|
console.log("process.env.DATABASE = ", process.env.DATABASE);
|
||||||
console.log("process.env.APPLE_APP_ID = ", process.env.APPLE_APP_ID);
|
console.log("process.env.APPLE_APP_ID = ", process.env.APPLE_APP_ID);
|
||||||
|
|
||||||
logger.info("App started on " + process.env.PROTOCOL + "://" + process.env.HOST + ":" + process.env.PORT + "");
|
logger.info("App started on " + process.env.PROTOCOL + "://" + process.env.HOST + ":" + process.env.PORT + "");
|
||||||
|
logger.info("process.env.NEXT_PUBLIC_PUBLIC_URL = ", process.env.NEXT_PUBLIC_PUBLIC_URL);
|
||||||
|
logger.info("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
|
||||||
|
logger.info("process.env.PORT = ", process.env.PORT);
|
||||||
|
logger.info("process.env.DATABASE = ", process.env.DATABASE);
|
||||||
logger.info("process.env.GIT_COMMIT_ID = " + process.env.GIT_COMMIT_ID);
|
logger.info("process.env.GIT_COMMIT_ID = " + process.env.GIT_COMMIT_ID);
|
||||||
logger.info("process.env.APP_ENV = " + process.env.APP_ENV);
|
logger.info("process.env.APP_ENV = " + process.env.APP_ENV);
|
||||||
|
logger.info("process.env.ENV_ENV = " + process.env.ENV_ENV);
|
||||||
logger.info("process.env.NODE_ENV = " + process.env.NODE_ENV);
|
logger.info("process.env.NODE_ENV = " + process.env.NODE_ENV);
|
||||||
logger.info("process.env.APPLE_APP_ID = " + process.env.APPLE_APP_ID);
|
logger.info("process.env.APPLE_APP_ID = " + process.env.APPLE_APP_ID);
|
||||||
logger.info("process.env.EMAIL_SERVICE = " + process.env.EMAIL_SERVICE);
|
logger.info("process.env.EMAIL_SERVICE = " + process.env.EMAIL_SERVICE);
|
||||||
@ -118,6 +124,12 @@ nextApp
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
server.use("/favicon.ico", express.static("public/favicon.png"));
|
server.use("/favicon.ico", express.static("public/favicon.png"));
|
||||||
|
// serve the same image for pwa-192x192.png and pwa-512x512.png
|
||||||
|
server.use("/pwa-192x192.png", express.static("public/favicon.png"));
|
||||||
|
server.use("/pwa-512x512.png", express.static("public/favicon.png"));
|
||||||
|
server.use("/manifest.json", express.static("public/manifest.json"));
|
||||||
|
//all static files are served from the public folder, including subfolders
|
||||||
|
server.use(express.static("public")); //ToDo: not working for some reason
|
||||||
// server.use("/robots.txt", express.static("styles/favicon_io/robots.txt"));
|
// server.use("/robots.txt", express.static("styles/favicon_io/robots.txt"));
|
||||||
// server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
|
// server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
|
||||||
|
|
||||||
@ -336,8 +348,11 @@ nextApp
|
|||||||
placeOfEvent: shift.cartEvent.location.name,
|
placeOfEvent: shift.cartEvent.location.name,
|
||||||
time: time,
|
time: time,
|
||||||
//bold the text after - in the notes
|
//bold the text after - in the notes
|
||||||
notes: shift.notes.substring(0, shift.notes.indexOf("-") + 1),
|
//notes: shift.notes.substring(0, shift.notes.indexOf("-") + 1),
|
||||||
notes_bold: shift.notes.substring(shift.notes.indexOf("-") + 1),
|
//notes_bold: shift.notes.substring(shift.notes.indexOf("-") + 1),
|
||||||
|
notes: shift.assignments.some(a => a.isWithTransport) ? "Транспорт: " : "",
|
||||||
|
notes_bold: shift.assignments.filter(a => a.isWithTransport).map(a => common.getInitials(a.publisher.firstName + " " + a.publisher.lastName)).join(", "),
|
||||||
|
|
||||||
names: shift.assignments
|
names: shift.assignments
|
||||||
.map((assignment) => {
|
.map((assignment) => {
|
||||||
return (
|
return (
|
||||||
|
@ -5,9 +5,10 @@ import { applyAuthTokenInterceptor } from 'axios-jwt';
|
|||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: common.getBaseUrl(),
|
baseURL: common.getBaseUrl(),
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
// headers: {
|
headers: {
|
||||||
// "Content-Type": "application/json",
|
// "Content-Type": "application/json",
|
||||||
// },
|
"Content-Encoding": "utf-8"
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Define token refresh function.
|
// 2. Define token refresh function.
|
||||||
@ -23,7 +24,6 @@ applyAuthTokenInterceptor(axiosInstance, { requestRefresh }); // Notice that th
|
|||||||
// 4. Logging in
|
// 4. Logging in
|
||||||
const login = async (params) => {
|
const login = async (params) => {
|
||||||
const response = await axiosInstance.post('/api/auth/signin', params)
|
const response = await axiosInstance.post('/api/auth/signin', params)
|
||||||
|
|
||||||
// save tokens to storage
|
// save tokens to storage
|
||||||
setAuthTokens({
|
setAuthTokens({
|
||||||
accessToken: response.data.access_token,
|
accessToken: response.data.access_token,
|
||||||
|
@ -31,7 +31,8 @@ const axiosServer = async (context) => {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//redirect to next-auth login page
|
//redirect to next-auth login page
|
||||||
context.res.writeHead(302, { Location: '/api/auth/signin' });
|
//context.res.writeHead(302, { Location: encodeURIComponent('/api/auth/signin') });
|
||||||
|
|
||||||
context.res.end();
|
context.res.end();
|
||||||
return { props: {} };
|
return { props: {} };
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ const { PrismaClient, UserRole } = require('@prisma/client');
|
|||||||
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const { getSession } = require("next-auth/react");
|
const { getSession } = require("next-auth/react");
|
||||||
|
const { DateTime, FixedOffsetZone } = require('luxon');
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info', // Set the default log level
|
level: 'info', // Set the default log level
|
||||||
@ -36,6 +36,27 @@ exports.logger = logger;
|
|||||||
// dotenv.config();
|
// dotenv.config();
|
||||||
// // dotenv.config({ path: ".env.local" });
|
// // dotenv.config({ path: ".env.local" });
|
||||||
|
|
||||||
|
exports.adjustUtcTimeToSofia = function (time) {
|
||||||
|
// Convert the Date object to a Luxon DateTime object in UTC
|
||||||
|
let result = DateTime.fromJSDate(time, { zone: 'utc' });
|
||||||
|
// Convert to Sofia time, retaining the local time as provided
|
||||||
|
result = result.setZone('Europe/Sofia', { keepLocalTime: true });
|
||||||
|
// Set hours, minutes, and seconds to match the input time
|
||||||
|
result = result.set({
|
||||||
|
hour: time.getHours(),
|
||||||
|
minute: time.getMinutes(),
|
||||||
|
second: time.getSeconds()
|
||||||
|
});
|
||||||
|
return result.toJSDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.adjustTimeToUTC = function (time) {
|
||||||
|
let result = DateTime.fromJSDate(time, { zone: 'Europe/Sofia' });
|
||||||
|
result = result.setZone('utc', { keepLocalTime: true });
|
||||||
|
return result.toJSDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.isValidPhoneNumber = function (phone) {
|
exports.isValidPhoneNumber = function (phone) {
|
||||||
if (typeof phone !== 'string') {
|
if (typeof phone !== 'string') {
|
||||||
return false; // or handle as you see fit
|
return false; // or handle as you see fit
|
||||||
@ -56,17 +77,6 @@ exports.isValidPhoneNumber = function (phone) {
|
|||||||
// If neither condition is met, the phone number is invalid
|
// If neither condition is met, the phone number is invalid
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
exports.setBaseUrl = function (req) {
|
|
||||||
const protocol = req.headers['x-forwarded-proto'] || 'http';
|
|
||||||
const host = req.headers.host;
|
|
||||||
const baseUrl = `${protocol}://${host}`;
|
|
||||||
|
|
||||||
// Write the baseUrl to the file
|
|
||||||
if (req != null) {
|
|
||||||
fs.writeFileSync(path.join(__dirname, 'baseUrl.txt'), baseUrl, 'utf8');
|
|
||||||
}
|
|
||||||
return baseUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getBaseUrl = function (relative = "", req = null) {
|
exports.getBaseUrl = function (relative = "", req = null) {
|
||||||
return process.env.NEXT_PUBLIC_PUBLIC_URL + relative;
|
return process.env.NEXT_PUBLIC_PUBLIC_URL + relative;
|
||||||
@ -329,14 +339,8 @@ exports.compareTimes = function (time1, time2) {
|
|||||||
const time2String = `${getHours(time2)}:${getMinutes(time2)}`;
|
const time2String = `${getHours(time2)}:${getMinutes(time2)}`;
|
||||||
return time1String.localeCompare(time2String);
|
return time1String.localeCompare(time2String);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.normalizeTime = function (date, baseDate) {
|
exports.normalizeTime = function (date, baseDate) {
|
||||||
// return set(baseDate, {
|
|
||||||
// hours: getHours(date),
|
|
||||||
// minutes: getMinutes(date),
|
|
||||||
// seconds: getSeconds(date),
|
|
||||||
// milliseconds: 0
|
|
||||||
// });
|
|
||||||
//don't use date-fns
|
|
||||||
let newDate = new Date(baseDate);
|
let newDate = new Date(baseDate);
|
||||||
newDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), 0);
|
newDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), 0);
|
||||||
return newDate;
|
return newDate;
|
||||||
@ -366,10 +370,7 @@ exports.getDateFormatedShort = function (date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.getTimeFomatted = function (date) {
|
|
||||||
return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });//timeZone: 'local'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*Todo: remove:
|
/*Todo: remove:
|
||||||
toISOString
|
toISOString
|
||||||
@ -528,7 +529,7 @@ exports.fuzzySearch = function (publishers, searchQuery, distanceThreshold = 0.9
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.getCurrentNonthFormatted = function () {
|
exports.getCurrentMonthFormatted = function () {
|
||||||
const getCurrentYearMonth = () => {
|
const getCurrentYearMonth = () => {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
@ -544,51 +545,140 @@ 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
|
|
||||||
exports.formatTimeHHmm = function (input) {
|
|
||||||
// Check if the input is a string or a Date object
|
|
||||||
const date = (typeof input === 'string') ? new Date(input) : input;
|
|
||||||
|
|
||||||
return date.toLocaleTimeString('en-US', {
|
|
||||||
hour12: false,
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
timeZone: 'Europe/Sofia'
|
|
||||||
}).substring(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//parse 'HH:mm' time string to date object
|
// new date FNs
|
||||||
exports.parseTimeHHmm = (timeString) => {
|
/**
|
||||||
// If timeString is already a date, return it as is
|
* Parses an input into a Luxon DateTime object, setting the timezone to 'Europe/Sofia' while keeping the local time.
|
||||||
if (timeString instanceof Date) {
|
* @param {string|Date} input - The input date string or JavaScript Date object.
|
||||||
return timeString;
|
* @returns {DateTime} - A Luxon DateTime object with the timezone set to 'Europe/Sofia', preserving the local time.
|
||||||
}
|
*/
|
||||||
|
const parseDate = (input) => {
|
||||||
|
let dateTime;
|
||||||
|
|
||||||
const [hours, minutes] = timeString.split(':');
|
if (input instanceof DateTime) {
|
||||||
const date = new Date();
|
// If input is already a Luxon DateTime, we adjust the zone only.
|
||||||
date.setHours(hours);
|
dateTime = input.setZone('Europe/Sofia');
|
||||||
date.setMinutes(minutes);
|
} else if (typeof input === 'string' || input instanceof Date) {
|
||||||
return date;
|
// Create a DateTime from the input assuming local timezone to preserve local time when changing the zone.
|
||||||
}
|
dateTime = DateTime.fromJSDate(new Date(input), { zone: 'local' });
|
||||||
|
dateTime = dateTime.setZone('Europe/Sofia');
|
||||||
exports.setTimeHHmm = (date, timeStringOrHours) => {
|
|
||||||
const newDate = new Date(date);
|
|
||||||
|
|
||||||
if (typeof timeStringOrHours === 'string' && timeStringOrHours.includes(':')) {
|
|
||||||
// If hours is a string in "HH:mm" format
|
|
||||||
const [h, m] = timeStringOrHours.split(':');
|
|
||||||
newDate.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0);
|
|
||||||
} else {
|
} else {
|
||||||
// If hours and minutes are provided separately
|
// Use the current time if no input is given, considered as local time.
|
||||||
newDate.setHours(parseInt(timeStringOrHours, 10), 0, 0, 0);
|
dateTime = DateTime.local().setZone('Europe/Sofia');
|
||||||
}
|
}
|
||||||
|
|
||||||
return newDate;
|
// Set the timezone to 'Europe/Sofia' while keeping the original local time.
|
||||||
|
return dateTime.setZone('Europe/Sofia', { keepLocalTime: true });
|
||||||
};
|
};
|
||||||
|
exports.parseDate = parseDate;
|
||||||
|
|
||||||
|
// Set timezone to 'Europe/Sofia' without translating time
|
||||||
|
exports.setTimezone = (input) => {
|
||||||
|
let dateTime = parseDate(input);
|
||||||
|
dateTime = dateTime.setZone('Europe/Sofia', { keepLocalTime: true });
|
||||||
|
return dateTime.toJSDate();
|
||||||
|
};
|
||||||
|
exports.setTime = (baseDateTime, timeDateTime) => {
|
||||||
|
// Ensure both inputs are DateTime objects
|
||||||
|
baseDateTime = parseDate(baseDateTime);
|
||||||
|
timeDateTime = parseDate(timeDateTime);
|
||||||
|
|
||||||
|
return baseDateTime.set({
|
||||||
|
hour: timeDateTime.hour,
|
||||||
|
minute: timeDateTime.minute,
|
||||||
|
second: timeDateTime.second,
|
||||||
|
millisecond: timeDateTime.millisecond
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format date to a specified format, defaulting to 'HH:mm'
|
||||||
|
exports.getTimeFormatted = (input, format = 'HH:mm') => {
|
||||||
|
const dateTime = parseDate(input);
|
||||||
|
return dateTime.toFormat(format);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set time in 'HH:mm' format to a date and return as JS Date in Sofia timezone
|
||||||
|
exports.setTimeHHmm = (input, timeString) => {
|
||||||
|
let dateTime = parseDate(input);
|
||||||
|
const [hour, minute] = timeString.split(':').map(Number);
|
||||||
|
dateTime = dateTime.set({ hour, minute });
|
||||||
|
return dateTime.toJSDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse 'HH:mm' time string to a JS Date object in Sofia timezone for today
|
||||||
|
exports.parseTimeHHmm = (timeString) => {
|
||||||
|
const dateTime = DateTime.now({ zone: 'Europe/Sofia' });
|
||||||
|
const [hour, minute] = timeString.split(':').map(Number);
|
||||||
|
return dateTime.set({ hour, minute }).toJSDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
function isTimeBetween(startTime, endTime, checkTime) {
|
||||||
|
const start = new Date(0, 0, 0, startTime.getHours(), startTime.getMinutes());
|
||||||
|
const end = new Date(0, 0, 0, endTime.getHours(), endTime.getMinutes());
|
||||||
|
const check = new Date(0, 0, 0, checkTime.getHours(), checkTime.getMinutes());
|
||||||
|
|
||||||
|
// If the end time is less than the start time, it means the time range spans midnight
|
||||||
|
if (end < start) {
|
||||||
|
// Check time is between start and midnight or between midnight and end
|
||||||
|
return (check >= start && check <= new Date(0, 0, 1, 0, 0, 0)) || (check >= new Date(0, 0, 0, 0, 0, 0) && check <= end);
|
||||||
|
} else {
|
||||||
|
return check >= start && check <= end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.isTimeBetween = isTimeBetween;
|
||||||
|
|
||||||
|
|
||||||
|
// ToDo: update all uses of this function to use the new one
|
||||||
|
|
||||||
|
// exports.getTimeFormatted = function (date) {
|
||||||
|
// const dateTime = DateTime.fromJSDate(date, { zone: 'Europe/Sofia' });
|
||||||
|
// return dateTime.toFormat('HH:mm');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// exports.setTimeHHmm = (date, timeStringOrHours) => {
|
||||||
|
// const newDate = new Date(date);
|
||||||
|
|
||||||
|
// if (typeof timeStringOrHours === 'string' && timeStringOrHours.includes(':')) {
|
||||||
|
// // If hours is a string in "HH:mm" format
|
||||||
|
// const [h, m] = timeStringOrHours.split(':');
|
||||||
|
// newDate.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0);
|
||||||
|
// } else {
|
||||||
|
// // If hours and minutes are provided separately
|
||||||
|
// newDate.setHours(parseInt(timeStringOrHours, 10), 0, 0, 0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return newDate;
|
||||||
|
// };
|
||||||
|
// // format date to 'HH:mm' time string required by the time picker
|
||||||
|
// exports.formatTimeHHmm = function (input) {
|
||||||
|
// // Check if the input is a string or a Date object
|
||||||
|
// const date = (typeof input === 'string') ? new Date(input) : input;
|
||||||
|
|
||||||
|
// return date.toLocaleTimeString('en-US', {
|
||||||
|
// hour12: false,
|
||||||
|
// hour: '2-digit',
|
||||||
|
// minute: '2-digit',
|
||||||
|
// timeZone: 'Europe/Sofia'
|
||||||
|
// }).substring(0, 5);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// //parse 'HH:mm' time string to date object
|
||||||
|
// exports.parseTimeHHmm = (timeString) => {
|
||||||
|
// // If timeString is already a date, return it as is
|
||||||
|
// if (timeString instanceof Date) {
|
||||||
|
// return timeString;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const [hours, minutes] = timeString.split(':');
|
||||||
|
// const date = new Date();
|
||||||
|
// date.setHours(hours);
|
||||||
|
// date.setMinutes(minutes);
|
||||||
|
// return date;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exports.getTimeInMinutes = (dateOrTimestamp) => {
|
exports.getTimeInMinutes = (dateOrTimestamp) => {
|
||||||
const date = new Date(dateOrTimestamp);
|
const date = new Date(dateOrTimestamp);
|
||||||
@ -775,8 +865,13 @@ exports.convertDatesToISOStrings = function (obj) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (obj instanceof Date) {
|
||||||
|
// return obj.toISOString();
|
||||||
|
// }
|
||||||
if (obj instanceof Date) {
|
if (obj instanceof Date) {
|
||||||
return obj.toISOString();
|
// Convert the Date object to a Luxon DateTime in UTC
|
||||||
|
const utcDate = DateTime.fromJSDate(obj, { zone: 'utc' });
|
||||||
|
return utcDate.toISO(); // Output in UTC as ISO string
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
@ -793,8 +888,43 @@ exports.convertDatesToISOStrings = function (obj) {
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
function adjustDateForDST(date, timezone) {
|
||||||
|
// Convert the date to the specified timezone
|
||||||
|
let dateTime = DateTime.fromJSDate(date, { zone: timezone });
|
||||||
|
|
||||||
|
// Check if the original date is in DST
|
||||||
|
const isOriginalDST = dateTime.isInDST;
|
||||||
|
|
||||||
|
// Check if the current date in the same timezone is in DST
|
||||||
|
const isNowDST = DateTime.now().setZone(timezone).isInDST;
|
||||||
|
|
||||||
|
// Compare and adjust if necessary
|
||||||
|
if (isOriginalDST && !isNowDST) {
|
||||||
|
// If original date was in DST but now is not, subtract one hour
|
||||||
|
dateTime = dateTime.minus({ hours: 1 });
|
||||||
|
} else if (!isOriginalDST && isNowDST) {
|
||||||
|
// If original date was not in DST but now is, add one hour
|
||||||
|
dateTime = dateTime.plus({ hours: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the adjusted date as a JavaScript Date
|
||||||
|
return dateTime.toJSDate();
|
||||||
|
}
|
||||||
|
exports.adjustDateForDST = adjustDateForDST;
|
||||||
|
|
||||||
|
|
||||||
|
exports.base64ToUint8Array = function (base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
const rawData = atob(base64);
|
||||||
|
const buffer = new Uint8Array(rawData.length);
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
buffer[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
// exports.getInitials = function (names) {
|
// exports.getInitials = function (names) {
|
||||||
// const parts = names.split(' '); // Split the full name into parts
|
// const parts = names.split(' '); // Split the full name into parts
|
||||||
// if (parts.length === 0) {
|
// if (parts.length === 0) {
|
||||||
|
@ -158,7 +158,7 @@ async function getAvailabilities(userId) {
|
|||||||
...item,
|
...item,
|
||||||
startTime: item.startTime.toISOString(),
|
startTime: item.startTime.toISOString(),
|
||||||
endTime: item.endTime.toISOString(),
|
endTime: item.endTime.toISOString(),
|
||||||
name: common.getTimeFomatted(item.startTime) + "-" + common.getTimeFomatted(item.endTime),
|
name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime),
|
||||||
//endDate can be null
|
//endDate can be null
|
||||||
endDate: item.endDate ? item.endDate.toISOString() : null,
|
endDate: item.endDate ? item.endDate.toISOString() : null,
|
||||||
type: 'availability',
|
type: 'availability',
|
||||||
@ -214,7 +214,7 @@ async function getAvailabilities(userId) {
|
|||||||
endTime: item.shift.endTime.toISOString(),
|
endTime: item.shift.endTime.toISOString(),
|
||||||
// name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "),
|
// name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "),
|
||||||
//name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "),
|
//name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "),
|
||||||
name: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
|
name: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)),
|
||||||
type: 'assignment',
|
type: 'assignment',
|
||||||
//delete shift object
|
//delete shift object
|
||||||
shift: null,
|
shift: null,
|
||||||
@ -614,7 +614,7 @@ function convertShiftDates(assignments) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getCalendarEvents(publisherId, date, availabilities = true, assignments = true) {
|
async function getCalendarEvents(publisherId, availabilities = true, assignments = true, includeUnpublished = false) {
|
||||||
const result = [];
|
const result = [];
|
||||||
// let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", date, assignments, availabilities, date ? true : false, publisherId);
|
// let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", date, assignments, availabilities, date ? true : false, publisherId);
|
||||||
|
|
||||||
@ -647,6 +647,7 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig
|
|||||||
assignments: {
|
assignments: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
// publisherId: true,
|
||||||
shift: {
|
shift: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -665,7 +666,7 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig
|
|||||||
publisher.availabilities?.forEach(item => {
|
publisher.availabilities?.forEach(item => {
|
||||||
result.push({
|
result.push({
|
||||||
...item,
|
...item,
|
||||||
title: common.getTimeFomatted(new Date(item.startTime)) + "-" + common.getTimeFomatted(new Date(item.endTime)), //item.name,
|
title: common.getTimeFormatted(new Date(item.startTime)) + "-" + common.getTimeFormatted(new Date(item.endTime)), //item.name,
|
||||||
date: new Date(item.startTime),
|
date: new Date(item.startTime),
|
||||||
startTime: new Date(item.startTime),
|
startTime: new Date(item.startTime),
|
||||||
endTime: new Date(item.endTime),
|
endTime: new Date(item.endTime),
|
||||||
@ -681,23 +682,21 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig
|
|||||||
//only published shifts
|
//only published shifts
|
||||||
|
|
||||||
publisher.assignments?.filter(
|
publisher.assignments?.filter(
|
||||||
assignment => assignment.shift.isPublished
|
assignment => assignment.shift.isPublished || includeUnpublished
|
||||||
).forEach(item => {
|
).forEach(item => {
|
||||||
result.push({
|
result.push({
|
||||||
...item,
|
...item,
|
||||||
title: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
|
title: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)),
|
||||||
date: new Date(item.shift.startTime),
|
date: new Date(item.shift.startTime),
|
||||||
startTime: new Date(item.shift.startTime),
|
startTime: new Date(item.shift.startTime),
|
||||||
endTime: new Date(item.shift.endTime),
|
endTime: new Date(item.shift.endTime),
|
||||||
publisherId: item.publisherid,
|
// publisherId: item.publisherId,
|
||||||
|
publisherId: publisher.id,
|
||||||
type: "assignment",
|
type: "assignment",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
workbox-config.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
|
||||||
|
|
||||||
|
// // ToDo: probably not used now as we use next-pwa( check config)
|
||||||
|
// // Only import the modules you need; skip precaching and routing if not needed
|
||||||
|
|
||||||
|
// workbox.core.skipWaiting();
|
||||||
|
// workbox.core.clientsClaim();
|
||||||
|
|
||||||
|
// //workbox.precaching.cleanupOutdatedCaches();
|
||||||
|
// //disable precaching
|
||||||
|
// workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// module.exports = {
|
||||||
|
// // Other webpack config...
|
||||||
|
// plugins: [
|
||||||
|
// // Other plugins...
|
||||||
|
// new InjectManifest({
|
||||||
|
// // These are some common options, and not all are required.
|
||||||
|
// // Consult the docs for more info.
|
||||||
|
// exclude: [/.../, '...'],
|
||||||
|
// maximumFileSizeToCacheInBytes: 1 * 1024 * 1024,
|
||||||
|
// // swSrc: './worker.js',
|
||||||
|
// }),
|
||||||
|
// ],
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Example: Set up push notification handling
|
||||||
|
// self.addEventListener('push', event => {
|
||||||
|
// console.log('Push event received at workbox.config: ', event);
|
||||||
|
// const data = event.data.json();
|
||||||
|
// event.waitUntil(
|
||||||
|
// self.registration.showNotification(data.title, {
|
||||||
|
// body: data.message,
|
||||||
|
// icon: '/path/to/icon.png'
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// });
|
71
worker.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// 'use strict'
|
||||||
|
// // currently not used as we ise next-pwa and in next.config.js we have withPWA.
|
||||||
|
// // maybe we can have withPWA({sw: './worker.js'}) ?
|
||||||
|
// console.log('Service Worker worker/index.js Loaded...')
|
||||||
|
// workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
|
||||||
|
// self.addEventListener('install', () => {
|
||||||
|
// console.log('service worker installed')
|
||||||
|
// });
|
||||||
|
|
||||||
|
// self.addEventListener('activate', () => {
|
||||||
|
// console.log('service worker activated')
|
||||||
|
// });
|
||||||
|
|
||||||
|
// self.addEventListener('fetch', (event) => {
|
||||||
|
// try {
|
||||||
|
// console.log('Fetch event for ', event.request.url);
|
||||||
|
// if (event.request.url.includes('/api/auth/callback/')) {
|
||||||
|
// // Use network only strategy for auth routes, or bypass SW completely
|
||||||
|
// event.respondWith(fetch(event.request));
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // other caching strategies...
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error(error)
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// self.addEventListener('push', function (event) {
|
||||||
|
// console.log('Push message', event)
|
||||||
|
// if (!(self.Notification && self.Notification.permission === 'granted')) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// const data = JSON.parse(event.data.text())
|
||||||
|
// event.waitUntil(
|
||||||
|
// registration.showNotification(data.title, {
|
||||||
|
// body: data.message,
|
||||||
|
// icon: '/icons/android-chrome-192x192.png'
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// self.addEventListener('notificationclick', function (event) {
|
||||||
|
// console.log('Notification click: tag', event.notification.tag)
|
||||||
|
// event.notification.close()
|
||||||
|
// event.waitUntil(
|
||||||
|
// clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
|
||||||
|
// if (clientList.length > 0) {
|
||||||
|
// let client = clientList[0]
|
||||||
|
// for (let i = 0; i < clientList.length; i++) {
|
||||||
|
// if (clientList[i].focused) {
|
||||||
|
// client = clientList[i]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return client.focus()
|
||||||
|
// }
|
||||||
|
// return clients.openWindow('/')
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // self.addEventListener('pushsubscriptionchange', function(event) {
|
||||||
|
// // event.waitUntil(
|
||||||
|
// // Promise.all([
|
||||||
|
// // Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true),
|
||||||
|
// // Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration))
|
||||||
|
// // .then(function(sub) { return saveSubscription(sub) })
|
||||||
|
// // ])
|
||||||
|
// // )
|
||||||
|
// // })
|
@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
console.log('Service Worker Loaded...')
|
console.log('SW /worker/index.js Loaded...')
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
try {
|
try {
|
||||||
@ -16,22 +16,44 @@ self.addEventListener('fetch', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('push', function (event) {
|
self.addEventListener('push', function (event) {
|
||||||
console.log('Push message', event)
|
console.log('SW: New push message', event)
|
||||||
if (!(self.Notification && self.Notification.permission === 'granted')) {
|
if (!(self.Notification && self.Notification.permission === 'granted')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = JSON.parse(event.data.text())
|
const data = JSON.parse(event.data.text())
|
||||||
|
console.log('SW: Push data', data)
|
||||||
|
actions: [
|
||||||
|
//font awesome icons
|
||||||
|
{ action: 'accept', title: 'Accept', icon: 'fa fa-check' },
|
||||||
|
{ action: 'decline', title: 'Decline', icon: 'fa fa-times' }
|
||||||
|
]
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
registration.showNotification(data.title, {
|
registration.showNotification(data.title, {
|
||||||
body: data.message,
|
body: data.message,
|
||||||
icon: '/icons/android-chrome-192x192.png'
|
icon: '/favicon.ico',
|
||||||
|
actions: [{ action: 'close', title: 'Close', icon: 'fa fa-times' }],
|
||||||
|
data: data.url,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener('notificationclick', function (event) {
|
self.addEventListener('notificationclick', function (event) {
|
||||||
console.log('Notification click: tag', event.notification.tag)
|
console.log('Notification click: tag', event.notification.tag, 'action', event.action)
|
||||||
event.notification.close()
|
event.notification.close()
|
||||||
|
switch (event.action) {
|
||||||
|
case 'accept':
|
||||||
|
console.log('User accepted the action.');
|
||||||
|
// handle acceptance
|
||||||
|
break;
|
||||||
|
case 'decline':
|
||||||
|
console.log('User declined the action.');
|
||||||
|
// handle decline
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// handle other cases
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
console.log(event)
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
|
||||||
if (clientList.length > 0) {
|
if (clientList.length > 0) {
|
||||||
|