diff --git a/.env b/.env index 5b30dbb..24dc32d 100644 --- a/.env +++ b/.env @@ -1,22 +1,13 @@ - #NODE_TLS_REJECT_UNAUTHORIZED='0' -SSL_ENABLED=false -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=localhost -NEXT_PUBLIC_PORT=3003 -NEXTAUTH_URL=https://localhost:3003 -# NEXTAUTH_URL_INTERNAL=http://127.0.0.1:3003 +# HOST=localhost +# PORT=3003 +# NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003 # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 +NODE_ENV=development # mysql -DATABASE_PROVIDER=mysql -# DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart_dev -DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev -# DATABASE_URL=mysql://cart:cartpw@20.101.62.76:3307/cart - -# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart # npx prisma migrate dev # // owner: dobromir.popov@gmail.com | Специално Свидетелстване София # // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 @@ -28,10 +19,13 @@ AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret v AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276 -APPLE_ID= -APPLE_TEAM_ID= +APPLE_ID=com.mwhitnessing.sofia +APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw +# to generate +APPLE_TEAM_ID=XC57P9SXDK +APPLE_KEY_ID=TB3V355G5Y APPLE_PRIVATE_KEY= -APPLE_KEY_ID= + AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x diff --git a/.env.demo b/.env.demo deleted file mode 100644 index 2a2aa9a..0000000 --- a/.env.demo +++ /dev/null @@ -1,37 +0,0 @@ -NODE_TLS_REJECT_UNAUTHORIZED='0' -# DATABASE_URL="file:./src/data/dev.db" -# DATABASE_URL="mysql://root:Zelen0ku4e@192.168.0.10:3306/cart" - -NEXT_PUBLIC_PORT= -# NEXT_PUBLIC_NEXTAUTH_URL=https://cart.d-popov.com -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=cart.d-popov.com -NEXTAUTH_URL=https://cart.d-popov.com -# NEXTAUTH_URL= https://demo.mwhitnessing.com - -# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 -NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 -DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart_demo - -APPLE_ID= -APPLE_TEAM_ID= -APPLE_PRIVATE_KEY= -APPLE_KEY_ID= - -AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK -AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x -AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com - -FACEBOOK_ID= -FACEBOOK_SECRET= - -GITHUB_ID= -GITHUB_SECRET= -# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com -# GOOGLE_SECRET=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 - -TWITTER_ID= -TWITTER_SECRET= - -EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525 -EMAIL_FROM=noreply@example.com diff --git a/.env.development b/.env.development index afec269..40806ac 100644 --- a/.env.development +++ b/.env.development @@ -1,15 +1,11 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 # NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +PROTOCOL=https +PORT=3003 +HOST=localhost +NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003 +# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev +DATABASE=mysql://cart:cartpw@localhost:3306/cart -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=localhost -NEXT_PUBLIC_PORT=3003 -NEXTAUTH_URL=https://localhost:3003 - -SSL_ENABLED=true -TELEGRAM_BOT=true SSL_KEY=./certificates/localhost-key.pem SSL_CERT=./certificates/localhost.pem - -DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev -# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart diff --git a/.env.development.raph b/.env.development.raph new file mode 100644 index 0000000..52d1a21 --- /dev/null +++ b/.env.development.raph @@ -0,0 +1,10 @@ +NODE_TLS_REJECT_UNAUTHORIZED=0 +# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert +PROTOCOL=http +PORT=3003 +HOST=localhost +NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003 +DATABASE="mysql://root:mdp-11000@127.0.0.1:3306/cart?connection_limit=5&charset=utf8mb4&collation=utf8mb4_unicode_ci" + +SSL_KEY=./certificates/localhost-key.pem +SSL_CERT=./certificates/localhost.pem diff --git a/.env.prod_staging b/.env.prod_staging deleted file mode 100644 index 4542e30..0000000 --- a/.env.prod_staging +++ /dev/null @@ -1,9 +0,0 @@ -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_PORT= -NEXT_PUBLIC_HOST=staging.mwhitnessing.com -NEXTAUTH_URL= https://staging.mwhitnessing.com - -# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 -NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 -# ? do we need to duplicate this? already defined in the deoployment yml file -DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb-staging:3306/jwpwsofia \ No newline at end of file diff --git a/.env.production b/.env.production index a3438f2..15d66db 100644 --- a/.env.production +++ b/.env.production @@ -1,9 +1,9 @@ -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_PORT= -NEXT_PUBLIC_HOST=sofia.mwhitnessing.com -NEXTAUTH_URL= https://sofia.mwhitnessing.com +PORT= +HOST=sofia.mwitnessing.com +PROTOCOL=http # we're behind a reverse proxy. SSL is handled by the proxy +NEXT_PUBLIC_PUBLIC_URL= https://sofia.mwitnessing.com # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 # ? do we need to duplicate this? already defined in the deoployment yml file -DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia \ No newline at end of file +DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia \ No newline at end of file diff --git a/.env.test b/.env.test index 1b9eef8..b99f907 100644 --- a/.env.test +++ b/.env.test @@ -1,13 +1,14 @@ -NODE_TLS_REJECT_UNAUTHORIZED='0' +NODE_ENV=test -NEXT_PUBLIC_PORT=5001 -NEXT_PUBLIC_PROTOCOL=https -NEXT_PUBLIC_HOST=cart.d-popov.com -NEXTAUTH_URL=https://cart.d-popov.com +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=ed8a9681efc414df89dfd03cd188ed58 -DATABASE_URL=mysql://cart:cartpw@192.168.0.10:3306/cart_dev +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 APPLE_ID= APPLE_TEAM_ID= @@ -23,14 +24,11 @@ FACEBOOK_SECRET= GITHUB_ID= GITHUB_SECRET= -GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com -GOOGLE_SECRET=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 +# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com +# GOOGLE_SECRET=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 TWITTER_ID= TWITTER_SECRET= EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525 EMAIL_FROM=noreply@example.com - -GMAIL_EMAIL_USERNAME= -GMAIL_EMAIL_APP_PASS= diff --git a/.vscode/launch.json b/.vscode/launch.json index b11332d..a2cc8ff 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,23 @@ "version": "0.2.0", "configurations": [ { - "name": "Run npm nodemon (DEV)", - "command": "npm run debug-env", + "name": "Run npm nodemon (DB)", + "command": "npm run debug", "request": "launch", "type": "node-terminal", "preLaunchTask": "killInspector", "env": { - "NODE_ENV": "development" + "APP_ENV": "development" + } + }, + { + "name": "Run npm nodemon (Raph)", + "command": "npm run debug", + "request": "launch", + "type": "node-terminal", + "preLaunchTask": "killInspector", + "env": { + "APP_ENV": "development.raph" } }, { @@ -35,7 +45,7 @@ "request": "launch", "type": "node-terminal", "cwd": "${workspaceFolder}", - "command": "conda activate node && npm run debug-env", + "command": "conda activate node && npm run debug", }, { "name": "Run conda npm TEST", diff --git a/DESIGN/readme.md b/DESIGN/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/_deploy/appleKey.p8 b/_deploy/appleKey.p8 new file mode 100644 index 0000000..4c686a8 --- /dev/null +++ b/_deploy/appleKey.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/ +wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ +c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6 +/KyhaFLl +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/_deploy/demo.10.yml b/_deploy/demo.10.yml index 307260e..06a3a49 100644 --- a/_deploy/demo.10.yml +++ b/_deploy/demo.10.yml @@ -10,7 +10,7 @@ services: - /mnt/apps/docker_volumes/cart/app/next-cart-app:/app environment: - NODE_ENV=demo - - DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart + - DATABASE=mysql://cart:cart2023@192.168.0.10:3306/cart #command: sh -c "apk update && apk add git && rm -rf /tmp/clone && git clone https://git.d-popov.com/popov/next-cart-app.git /tmp/clone && rm -rf /app/* && cp -R /tmp/clone/next-cart-app/* /app/ && rm -rf /tmp/clone && npm cache clean --force && rm -rf /app/node_modules /app/package-lock.json && npm --silent --prefix /app install /app && npx --prefix /app prisma generate && npm --prefix /app run test; tail -f /dev/null" #command: sh -c "rm -rf /tmp/clone && git clone https://git.d-popov.com/popov/next-cart-app.git /tmp/clone && rm -rf /app/* && cp -R /tmp/clone/next-cart-app/* /app/ && rm -rf /tmp/clone && npm cache clean --force && rm -rf /app/node_modules /app/package-lock.json && npm --silent --prefix /app install /app && npx --prefix /app prisma generate && npm --prefix /app run test; tail -f /dev/null" command: sh -c "npm cache clean --force && rm -rf /app/node_modules /app/package-lock.json && npm --silent --prefix /app install /app && npx --prefix /app prisma generate && npm --prefix /app run test; tail -f /dev/null" diff --git a/_deploy/demo.11-demo.yml b/_deploy/demo.11-demo.yml index 07a4a7f..3a3a4d9 100644 --- a/_deploy/demo.11-demo.yml +++ b/_deploy/demo.11-demo.yml @@ -8,7 +8,7 @@ services: - /mnt/apps/DEV/cart-demo:/app environment: - NODE_ENV=demo - - DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart + - DATABASE=mysql://cart:cart2023@192.168.0.10:3306/cart command: sh -c " cd /app && npm run test; tail -f /dev/null" tty: true stdin_open: true diff --git a/_deploy/deoloy.azure.demo.yml b/_deploy/deoloy.azure.demo.yml new file mode 100644 index 0000000..8dac626 --- /dev/null +++ b/_deploy/deoloy.azure.demo.yml @@ -0,0 +1,38 @@ +version: "3" +services: + nextjs-app: # https://sofia.mwhitnessing.com/ + hostname: jwpw-app-staging # jwpw-nextjs-app-1 + image: docker.d-popov.com/jwpw:latest + volumes: + - /mnt/docker_volumes/pw-demo/app/public/content/uploads/:/app/public/content/uploads + environment: + - APP_ENV=test + - NODE_ENV=test + - TZ=Europe/Sofia + - DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb-demo:3306/jwpwsofia_demo + - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git + - GIT_BRANCH=main + - GIT_USERNAME=deploy + - GIT_PASSWORD=L3Kr2R438u4F7 + command: sh -c " cd /app && npm install && npx next build && npm run nodeenv; tail -f /dev/null" + tty: true + stdin_open: true + restart: always + networks: + - infrastructure_default + mariadb: + deploy: + replicas: 1 + hostname: mariadb-demo + image: mysql:latest #mariadb:10.4 + volumes: + - /mnt/docker_volumes/pw-demo2/data/mysql:/var/lib/mysql + environment: + MARIADB_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93 + MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93 + MYSQL_DATABASE: jwpwsofia_demo + MYSQL_USER: jwpwsofia_demo + MYSQL_PASSWORD: dwxhns9p9vp248 +networks: + infrastructure_default: + external: true diff --git a/_deploy/deoloy.azure.prod.stage.yml b/_deploy/deoloy.azure.prod.stage.yml deleted file mode 100644 index 9590d67..0000000 --- a/_deploy/deoloy.azure.prod.stage.yml +++ /dev/null @@ -1,57 +0,0 @@ -version: "3" -services: - nextjs-app: # https://sofia.mwhitnessing.com/ - hostname: jwpw-app-staging # jwpw-nextjs-app-1 - image: docker.d-popov.com/jwpw:latest - volumes: - - /mnt/docker_volumes/pw-staging/app/public/content/uploads/:/app/public/content/uploads - environment: - - NODE_ENV=prod_staging - - TZ=Europe/Sofia - - DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb-staging:3306/jwpwsofia - - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git - - GIT_BRANCH=main - - GIT_USERNAME=deploy - - GIT_PASSWORD=L3Kr2R438u4F7 - command: sh -c " cd /app && npm install && npm run nodeenv; tail -f /dev/null" - tty: true - stdin_open: true - restart: always - # ports: - # - "3001:3000" - networks: - - infrastructure_default - mariadb: - hostname: mariadb-staging - image: mariadb:latest #mariadb:10.4 - volumes: - - /mnt/docker_volumes/pw-staging/data/mysql:/var/lib/mysql - environment: - MARIADB_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262 - MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262 - MYSQL_DATABASE: jwpwsofia - MYSQL_USER: jwpwsofia - MYSQL_PASSWORD: dwxhns9p9vp248V39xJyRthUsZ2gR9 - #command: ["mysqld", "--max-connections=1000", "--sql-mode=ALLOW_INVALID_DATES,ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO,HIGH_NOT_PRECEDENCE,IGNORE_SPACE,NO_AUTO_CREATE_USER,NO_AUTO_VALUE_ON_ZERO,NO_BACKSLASH_ESCAPES,NO_DIR_IN_CREATE,NO_ENGINE_SUBSTITUTION,NO_FIELD_OPTIONS,NO_KEY_OPTIONS,NO_TABLE_OPTIONS,NO_UNSIGNED_SUBTRACTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY,PIPES_AS_CONCAT,REAL_AS_FLOAT,STRICT_ALL_TABLES,STRICT_TRANS_TABLES,ANSI,DB2,MAXDB,MSSQL,MYSQL323,MYSQL40,ORACLE,POSTGRESQL,TRADITIONAL", "--wait-timeout=28800"] - networks: - - infrastructure_default - postgres: - hostname: postgres - image: postgres - restart: always - # set shared memory limit when using docker-compose - shm_size: 128mb - # or set shared memory limit when deploy via swarm stack - #volumes: - # - type: tmpfs - # target: /dev/shm - # tmpfs: - # size: 134217728 # 128*2^20 bytes = 128Mb - environment: - POSTGRES_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262 - - networks: - - infrastructure_default -networks: - infrastructure_default: - external: true diff --git a/_deploy/deoloy.azure.prod.yml b/_deploy/deoloy.azure.production.yml similarity index 87% rename from _deploy/deoloy.azure.prod.yml rename to _deploy/deoloy.azure.production.yml index 0cb1e8b..7f8bcdb 100644 --- a/_deploy/deoloy.azure.prod.yml +++ b/_deploy/deoloy.azure.production.yml @@ -1,8 +1,10 @@ version: "3" services: - nextjs-app: # https://sofia.mwhitnessing.com/ + nextjs-app: # https://sofia.mwitnessing.com/ hostname: jwpw-app # jwpw-nextjs-app-1 image: docker.d-popov.com/jwpw:latest + deploy: + replicas: 2 #ports: # - "3000:3000" volumes: @@ -10,8 +12,8 @@ services: environment: - NODE_ENV=production - TZ=Europe/Sofia - - DATABASE_URL=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia - #- DATABASE_URL=postgres://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia + - DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia + #- DATABASE=postgres://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git - GIT_BRANCH=production - GIT_USERNAME=deploy @@ -30,7 +32,7 @@ services: volumes: - /mnt/docker_volumes/pw/data/mysql:/var/lib/mysql environment: - MARIADB_ROOT_PASSWORD: dwxhns9p9vp248V39xJyRthUsZ2gR9 + # MARIADB_ROOT_PASSWORD: dwxhns9p9vp248V39xJyRthUsZ2gR9 MYSQL_ROOT_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262 MYSQL_DATABASE: jwpwsofia MYSQL_USER: jwpwsofia @@ -38,7 +40,6 @@ services: networks: - default - infrastructure_default - mariadb_backup: image: alpine:latest volumes: @@ -51,11 +52,11 @@ services: MYSQL_HOST: mariadb # GOOGLE_DRIVE_FOLDER_ID: your_google_drive_folder_id entrypoint: /bin/sh -c - networks: - infrastructure_default command: | - "apk add --no-cache mysql-client curl && \ + "apk update && \ + apk add --no-cache mariadb-client mariadb-connector-c && \ echo '0 2 * * * mysqldump -h $$MYSQL_HOST -P 3306 -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql' > /etc/crontabs/root && \ crond -f -d 8" # wget -q https://github.com/prasmussen/gdrive/releases/download/2.1.0/gdrive-linux-x64 -O /usr/bin/gdrive && \ diff --git a/_deploy/entrypoint.sh b/_deploy/entrypoint.sh index 8a7d603..f0ea095 100644 --- a/_deploy/entrypoint.sh +++ b/_deploy/entrypoint.sh @@ -10,7 +10,7 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then mkdir /tmp/clone # Clone the repository - git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwhitnessing.git /tmp/clone || exit 1 + git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwitnessing.git /tmp/clone || exit 1 # Synchronize all files except package.json and package-lock.json to /app rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files" diff --git a/_deploy/deploy.homelab.production.yml b/_deploy/homelab.deploy.production.yml similarity index 53% rename from _deploy/deploy.homelab.production.yml rename to _deploy/homelab.deploy.production.yml index cf14df8..3c1826a 100644 --- a/_deploy/deploy.homelab.production.yml +++ b/_deploy/homelab.deploy.production.yml @@ -6,7 +6,7 @@ services: - "5001:3000" environment: - NODE_ENV=prod - - DATABASE_URL=mysql://cart:o74x642Rc8@mariadb:3306/cart + - DATABASE=mysql://cart:o74x642Rc8@mariadb:3306/cart - UPDATE_CODE_FROM_GIT=true # Set to true to pull latest code from Git - GIT_USERNAME=deploy - GIT_PASSWORD=%L3Kr2R438u4F7^%40 @@ -15,7 +15,7 @@ services: stdin_open: true mariadb: hostname: mariadb - image: mariadb #bitnami/mariadb:latest #mariadb:10.4 + image: mariadb #bitnami/mariadb:latest #mariadb:10.4 environment: MARIADB_ROOT_PASSWORD: Pw62L$3332JH MYSQL_ROOT_PASSWORD: Pw62L$3332JH @@ -24,4 +24,20 @@ services: MYSQL_PASSWORD: o74x642Rc8 networks: - default - - mysql_default \ No newline at end of file + - mysql_default + postgres: + deploy: + replicas: 0 + hostname: postgres + image: postgres + restart: always + # set shared memory limit when using docker-compose + shm_size: 128mb + # or set shared memory limit when deploy via swarm stack + #volumes: + # - type: tmpfs + # target: /dev/shm + # tmpfs: + # size: 134217728 # 128*2^20 bytes = 128Mb + environment: + POSTGRES_PASSWORD: i4966cWBtP3xJ7BLsbsgo93C8Q5262 diff --git a/_deploy/prod.Dockerfile b/_deploy/prod.Dockerfile index f894520..14d7092 100644 --- a/_deploy/prod.Dockerfile +++ b/_deploy/prod.Dockerfile @@ -4,6 +4,8 @@ FROM node:current-alpine # Set environment variables for Node.js ENV NODE_ENV=production +# ENV MYSQL_ROOT_PASSWORD=pass +ENV MYSQL_DATABASE=cart # Create and set the working directory WORKDIR /app diff --git a/_deploy/sample.docker-compose.yml b/_deploy/sample.docker-compose.yml index 06a9ae4..32347a9 100644 --- a/_deploy/sample.docker-compose.yml +++ b/_deploy/sample.docker-compose.yml @@ -40,7 +40,7 @@ services: - /mnt/data/apps/docker_volumes/cart/app:/app environment: - NODE_ENV=demo - - DATABASE_URL=mysql://cart:cartpw2024@mariadb:3306/cart + - DATABASE=mysql://cart:cartpw2024@mariadb:3306/cart #! entrypoint: ["/bin/sh", "/entrypoint.sh"] #run: npm install && npx prisma generate && npm run test; # command: "npx prisma migrate deploy && npx prisma migrate deploy && npm run build && npm run start" diff --git a/_deploy/setupAppleId.mjs b/_deploy/setupAppleId.mjs new file mode 100644 index 0000000..51f9682 --- /dev/null +++ b/_deploy/setupAppleId.mjs @@ -0,0 +1,78 @@ +#!/bin/node + +import { SignJWT } from "jose" +import { createPrivateKey } from "crypto" + +if (process.argv.includes("--help") || process.argv.includes("-h")) { + console.log(` + Creates a JWT from the components found at Apple. + By default, the JWT has a 6 months expiry date. + Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048 + + Usage: + node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp] + APPLE_ID=com.mwhitnessing.sofia +APPLE_TEAM_ID=XC57P9SXDK +APPLE_KEY_ID=TB3V355G5Y +APPLE_KEY + +node setupAppleId.mjs --kid YOUR_KEY_ID --iss YOUR_TEAM_ID --private_key "$(cat key.p8)" --sub YOUR_CLIENT_ID --expires_in 15778800 +node setupAppleId.mjs --kid TB3V355G5Y --iss XC57P9SXDK --sub com.mwhitnessing.sofia --private_key "$(cat appleKey.p8)" + +>>Apple client secret generated. Valid until: Tue Oct 01 2024 00:05:43 GMT+0300 (Eastern European Summer Time) + +eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw + + + Options: + --help Print this help message + --kid, --key_id The key id of the private key + --iss, --team_id The Apple team ID + --private_key The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----) + --sub, --client_id The client id to use in the JWT. + --expires_in Number of seconds from now when the JWT should expire. Defaults to 6 months. + --exp Future date in seconds when the JWT expires + `) +} else { + const args = process.argv.slice(2).reduce((acc, arg, i) => { + if (arg.match(/^--\w/)) { + const key = arg.replace(/^--/, "").toLowerCase() + acc[key] = process.argv[i + 3] + } + return acc + }, {}) + + const { + team_id, + iss = team_id, + + private_key, + + client_id, + sub = client_id, + + key_id, + kid = key_id, + + expires_in = 86400 * 180, + exp = Math.ceil(Date.now() / 1000) + expires_in, + } = args + + /** + * How long is the secret valid in seconds. + * @default 15780000 + */ + const expiresAt = Math.ceil(Date.now() / 1000) + expires_in + const expirationTime = exp ?? expiresAt + console.log(` +Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)} + +${await new SignJWT({}) + .setAudience("https://appleid.apple.com") + .setIssuer(iss) + .setIssuedAt() + .setExpirationTime(expirationTime) + .setSubject(sub) + .setProtectedHeader({ alg: "ES256", kid }) + .sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`) +} \ No newline at end of file diff --git a/_doc/ToDo.md b/_doc/ToDo.md index ae6c5fd..b8801e8 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -187,3 +187,18 @@ fix availability repeat checks sometimes delete from mycalendar fails saturday shifts start at 12:00 / dymamic +------------------------------- +Add availability type UNAVAILABLE/ AWAY (like Estelle, Rick, Me) + +why "Александра Чернъшова" seems available every shift thursdays? +fix Time ZONE (currently Z, but it leads to shift when the DST changes ( winter entries are shifter in summer)) +защо Марсел Клайнер е червен четв 11 април? - има предпочитания и е в номата +fix repeating availabilities - Tanq kolcjanova only blue first thursday +add assignment in calendar planner +fix database + +-- +emails +mobile apps +apple login +разрешителни - upload diff --git a/_doc/apple-gen-secret.mjs b/_doc/apple-gen-secret.mjs new file mode 100644 index 0000000..4a6600c --- /dev/null +++ b/_doc/apple-gen-secret.mjs @@ -0,0 +1,67 @@ +#!/bin/node +# https://gist.githubusercontent.com/balazsorban44/09613175e7b37ec03f676dcefb7be5eb/raw/b0d31aa0c7f58e0088fdf59ec30cad1415a3475b/apple-gen-secret.mjs + +import { SignJWT } from "jose" +import { createPrivateKey } from "crypto" + +if (process.argv.includes("--help") || process.argv.includes("-h")) { + console.log(` + Creates a JWT from the components found at Apple. + By default, the JWT has a 6 months expiry date. + Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048 + + Usage: + node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp] + + Options: + --help Print this help message + --kid, --key_id The key id of the private key + --iss, --team_id The Apple team ID + --private_key The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----) + --sub, --client_id The client id to use in the JWT. + --expires_in Number of seconds from now when the JWT should expire. Defaults to 6 months. + --exp Future date in seconds when the JWT expires + `) +} else { + const args = process.argv.slice(2).reduce((acc, arg, i) => { + if (arg.match(/^--\w/)) { + const key = arg.replace(/^--/, "").toLowerCase() + acc[key] = process.argv[i + 3] + } + return acc + }, {}) + + const { + team_id, + iss = team_id, + + private_key, + + client_id, + sub = client_id, + + key_id, + kid = key_id, + + expires_in = 86400 * 180, + exp = Math.ceil(Date.now() / 1000) + expires_in, + } = args + + /** + * How long is the secret valid in seconds. + * @default 15780000 + */ + const expiresAt = Math.ceil(Date.now() / 1000) + expires_in + const expirationTime = exp ?? expiresAt + console.log(` +Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)} + +${await new SignJWT({}) + .setAudience("https://appleid.apple.com") + .setIssuer(iss) + .setIssuedAt() + .setExpirationTime(expirationTime) + .setSubject(sub) + .setProtectedHeader({ alg: "ES256", kid }) + .sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`) +} \ No newline at end of file diff --git a/_doc/notes.mb b/_doc/notes.mb index 4862741..a94440c 100644 --- a/_doc/notes.mb +++ b/_doc/notes.mb @@ -111,6 +111,11 @@ export OPENAI_API_KEY=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 # dev- # ----------------------------------------------update PRISMA schema/sync database ----------------------------------------------- # # prisma migrate dev --create-only +NODE_ENV=production npx prisma migrate deploy +#windows +$env:DATABASE="mysql://cart:cartpw@localhost:3306/cart"; npx prisma migrate deploy +$env:DATABASE="mysql://cart:cartpw@192.168.0.10:3306/cart_dev"; npx prisma migrate deploy + npx prisma generate npx prisma migrate dev --name fix_nextauth_schema --create-only >Prisma Migrate created the following migration without applying it 20231214163235_fix_nextauth_schema @@ -153,8 +158,13 @@ Remove-Item package-lock.json npm install -# -- mysql -# fix +# ---------------------------------- mysql ----------------------------------- # +#backup: (--no-data to skip data) +mysqldump -h mariadb -P 3306 -ujwpwsofia -p"dwxhns9p9vp248V39xJyRthUsZ2gR9" jwpwsofia --skip-add-locks > /backup/manual-$(date +\%Y-\%m-\%d-\%H\%M\%S)-$MYSQL_DATABASE.sql + + + +# fix++ mysql -u root -pi4966cWBtP3xJ7BLsbsgo93C8Q5262 -- mysqld_safe --skip-grant-tables & @@ -164,8 +174,15 @@ SET PASSWORD FOR 'root'@'localhost' = PASSWORD('i4966cWBtP3xJ7BLsbsgo93C8Q5262') GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.3' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION; +GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9' WITH GRANT OPTION; FLUSH PRIVILEGES; exit; +ALTER USER 'jwpwsofia'@'172.22.0.%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9'; +--if error (does not exist) +CREATE USER 'jwpwsofia'@'172.22.0.%' IDENTIFIED BY 'dwxhns9p9vp248V39xJyRthUsZ2gR9'; +GRANT ALL PRIVILEGES ON jwpwsofia.* TO 'jwpwsofia'@'172.22.0.%' WITH GRANT OPTION; + + #Install depcheck: @@ -184,3 +201,8 @@ ncu -u enable apple ID: curl https://gist.githubusercontent.com/balazsorban44/09613175e7b37ec03f676dcefb7be5eb/raw/b0d31aa0c7f58e0088fdf59ec30cad1415a3475b/apple-gen-secret.mjs -o apple-gen-secret.mjs + + + + +Project setup: diff --git a/components/ExampleForm.js b/components/ExampleForm.js index 45f49f8..bf9050f 100644 --- a/components/ExampleForm.js +++ b/components/ExampleForm.js @@ -40,7 +40,7 @@ class ExampleForm extends React.Component { } const [item, set] = useState({ - isactive: true, + isActive: true, }); const router = useRouter(); @@ -63,7 +63,7 @@ class ExampleForm extends React.Component { handleChange = ({ target }) => { - if (target.name === "isactive") { + if (target.name === "isActive") { set({ ...item, [target.name]: target.checked }); } else if (target.name === "age") { set({ ...item, [target.name]: parseInt(target.value) }); @@ -100,8 +100,8 @@ class ExampleForm extends React.Component {

{router.query?.id ? "Редактирай" : "Създай"} Item

- -
diff --git a/components/Modal.tsx b/components/Modal.tsx new file mode 100644 index 0000000..f52839b --- /dev/null +++ b/components/Modal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import common from '../src/helpers/common'; // Ensure this path is correct + +interface ModalProps { + children: React.ReactNode; + isOpen: boolean; + onClose: () => void; + forDate: Date; + useFilterDate: boolean; + onUseFilterDateChange: (value: boolean) => void; +} + +function Modal({ children, isOpen, onClose, forDate, useFilterDate, onUseFilterDateChange }: ModalProps) { + if (!isOpen) return null; + const isValidDate = forDate instanceof Date && !isNaN(forDate.getTime()); + console.log("forDate", forDate, isValidDate); + + return ( +
+
+ {isValidDate && ( +

+ +

+ )} + {children} + +
+
+
+ ); +} + +export default Modal; diff --git a/components/availability/AvailabilityForm.js b/components/availability/AvailabilityForm.js index c1fe4c7..f8f52c6 100644 --- a/components/availability/AvailabilityForm.js +++ b/components/availability/AvailabilityForm.js @@ -1,5 +1,5 @@ import axiosInstance from '../../src/axiosSecure'; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, use } from "react"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; @@ -9,6 +9,8 @@ import bg from 'date-fns/locale/bg'; import { bgBG } from '../x-date-pickers/locales/bgBG'; import { ToastContainer } from 'react-toastify'; const common = require('src/helpers/common'); +//todo import Availability type from prisma schema + const fetchConfig = async () => { const config = await import('../../config.json'); @@ -23,12 +25,12 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o indexUrl: "/cart/availabilities" }; + //coalsce existingItems to empty array + existingItems = existingItems || []; + const [editMode, setEditMode] = useState(existingItems.length > 0); const [publisher, setPublisher] = useState({ id: publisherId }); const [day, setDay] = useState(new Date(date)); - const [doRepeat, setDoRepeat] = useState(false); - const [repeatFrequency, setRepeatFrequency] = useState(1); - const [repeatUntil, setRepeatUntil] = useState(null); const [canUpdate, setCanUpdate] = useState(true); const [timeSlots, setTimeSlots] = useState([]); @@ -39,13 +41,17 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o dayOfMonth: null, // startTime: "08:00", // endTime: "20:00", - isactive: true, + isActive: true, repeatWeekly: false, endDate: null, isFirst: false, isLast: false, }]); + const [doRepeat, setDoRepeat] = useState(existingItems && existingItems.length > 0 ? existingItems[0].repeatWeekly : false); + const [repeatFrequency, setRepeatFrequency] = useState(1); + const [repeatUntil, setRepeatUntil] = useState(null); + const [isInline, setInline] = useState(inline || false); const [config, setConfig] = useState(null); useEffect(() => { @@ -56,30 +62,28 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o }, []); - - // Define the minimum and maximum times const minTime = new Date(); minTime.setHours(8, 0, 0, 0); // 8:00 AM const maxTime = new Date(); maxTime.setHours(20, 0, 0, 0); // 8:00 PM + const fetchItemFromDB = async () => { + const id = parseInt(router.query.id); + if (existingItems.length == 0 && id) { + try { + const response = await axiosInstance.get(`/api/data/availabilities/${id}`); + setAvailabilities([response.data]); + setEditMode(true); + setDoRepeat(response.data.repeatWeekly); + } catch (error) { + console.error(error); + toast.error("Error fetching availability data."); + } + } + }; useEffect(() => { - const fetchItemFromDB = async () => { - const id = parseInt(router.query.id); - if (existingItems.length == 0 && id) { - try { - const response = await axiosInstance.get(`/api/data/availabilities/${id}`); - setAvailabilities([response.data]); - setEditMode(true); - } catch (error) { - console.error(error); - toast.error("Error fetching availability data."); - } - } - }; - fetchItemFromDB(); }, [router.query.id]); @@ -88,32 +92,34 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o e.preventDefault(); try { const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots); - + let avs = availabilities.filter(av => av.type !== "assignment"); // Determine if we need to delete and recreate, or just update - const shouldRecreate = availabilities.length !== groupedTimeSlots.length || availabilities.some(av => !av.id); + let shouldRecreate = avs.length > 0 && avs.length !== groupedTimeSlots.length || avs.some(av => !av.id); + shouldRecreate = shouldRecreate || (avs.length == 0 && availabilities.length > 0); + //create availability if we open a form with assignment without availability if (shouldRecreate) { // Delete existing availabilities if they have an ID console.log("Recreating availabilities"); - await Promise.all(availabilities.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`))); + await Promise.all(avs.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`))); // Create new availabilities - const createdAvailabilities = await Promise.all(groupedTimeSlots.map(async group => { + avs = await Promise.all(groupedTimeSlots.map(async group => { const newAvailability = createAvailabilityFromGroup(group, publisher.id); const response = await axiosInstance.post(urls.apiUrl, newAvailability); return response.data; // Assuming the new availability is returned })); - setAvailabilities(createdAvailabilities); + setAvailabilities(avs); } else { // Update existing availabilities console.log("Updating existing availabilities"); - const updatedAvailabilities = await Promise.all(availabilities.map(async (availability, index) => { + avs = await Promise.all(avs.map(async (availability, index) => { const group = groupedTimeSlots[index]; const id = availability.id; const updatedAvailability = updateAvailabilityFromGroup(availability, group); delete updatedAvailability.id; - delete updatedAvailability.type; + //delete updatedAvailability.type; delete updatedAvailability.publisherId; delete updatedAvailability.title; delete updatedAvailability.date; @@ -122,7 +128,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return updatedAvailability; })); - setAvailabilities(updatedAvailabilities); + setAvailabilities(avs); } handleCompletion({ updated: true }); @@ -172,54 +178,123 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return groupedIntervals; } - // const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport; - // const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport; - function createAvailabilityFromGroup(group) { - let startTime = new Date(day); + // // const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport; + // // const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport; + // function createAvailabilityFromGroup(group) { + // let startTime = new Date(day); + // startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0); + + // let endTime = new Date(day); + // endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0); + + + // return { + // name: common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime), + // publisherId: publisher.id, + // startTime: startTime, + // endTime: endTime, + // isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport, + // isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport, + // dayofweek: common.getDayOfWeekNameEnEnumForDate(day.getDay()), + // repeatWeekly: doRepeat, + // dayOfMonth: doRepeat ? null : startTime.getDate(), + // endDate: doRepeat ? repeatUntil : null, + // dateOfEntry: new Date(), + // }; + // } + + // function updateAvailabilityFromGroup(availability, group) { + // availability.startTime.setTime(group[0].startTime); + // availability.endTime.setTime(group[group.length - 1].endTime); + // availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime); + + // availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport; + // availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport; + + // delete availability.weekOfMonth; + // if (doRepeat) { + // availability.repeatWeekly = true; + // availability.dayOfMonth = null; + // availability.weekOfMonth = 0; + // availability.endDate = repeatUntil; + // } else { + // availability.repeatWeekly = false; + // availability.dayOfMonth = availability.startTime.getDate(); + // availability.endDate = null; + // } + + // availability.dateOfEntry = new Date(); + // if (availability.parentAvailabilityId) { + // availability.parentAvailability = { connect: { id: parentAvailabilityId } }; + // } + // delete availability.parentAvailabilityId; + + // return availability; + // } + // Common function to set shared properties + function setSharedAvailabilityProperties(availability, group, timeSlots) { + let startTime = new Date(availability.startTime || day); startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0); - let endTime = new Date(day); + let endTime = new Date(availability.endTime || day); endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0); - - return { - name: common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime), - publisherId: publisher.id, - startTime: startTime, - endTime: endTime, - isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport, - isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport, - dayofweek: common.getDayOfWeekNameEnEnum(day.getDay()), - repeatWeekly: doRepeat, - dayOfMonth: doRepeat ? null : startTime.getDate(), - endDate: doRepeat ? repeatUntil : null, - dateOfEntry: new Date(), - }; - } - - function updateAvailabilityFromGroup(availability, group) { - availability.startTime.setTime(group[0].startTime); - availability.endTime.setTime(group[group.length - 1].endTime); - availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime); + availability.startTime = startTime; + availability.endTime = endTime; + availability.name = common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime); availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport; availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport; - availability.repeatWeekly = doRepeat; - availability.dayOfMonth = doRepeat ? null : availability.startTime.getDate(); - availability.endDate = doRepeat ? repeatUntil : null; + // Adjustments for repeating settings + if (doRepeat) { + availability.repeatWeekly = true; + availability.type = "Weekly" + availability.dayOfMonth = null; + availability.endDate = repeatUntil; + } else { + availability.type = "OneTime" + availability.repeatWeekly = false; + availability.dayOfMonth = startTime.getDate(); + availability.endDate = null; + } + availability.dateOfEntry = new Date(); + } + + function createAvailabilityFromGroup(group) { + let availability = { + publisherId: publisher.id, + dayofweek: common.getDayOfWeekNameEnEnumForDate(day), + }; + + setSharedAvailabilityProperties(availability, group, timeSlots); + return availability; } + function updateAvailabilityFromGroup(availability, group) { + setSharedAvailabilityProperties(availability, group, timeSlots); + delete availability.weekOfMonth; + if (doRepeat) { + availability.weekOfMonth = 0; + } + if (availability.parentAvailabilityId) { + availability.parentAvailability = { connect: { id: parentAvailabilityId } }; + } + delete availability.parentAvailabilityId; + + return availability; + } const handleDelete = async (e) => { e.preventDefault(); try { - const deletePromises = availabilities.map(async (availability) => { + let avs = availabilities.filter(av => av.type !== "assignment"); + const deletePromises = avs.map(async (availability) => { if (availability.id) { // console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id); await axiosInstance.delete(urls.apiUrl + availability.id); @@ -233,9 +308,10 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o handleCompletion({ deleted: true }); } } catch (error) { - alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас"); + //alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас"); console.log(JSON.stringify(error)); toast.error(error.response?.data?.message || "An error occurred"); + fetchItemFromDB(); } }; @@ -294,8 +370,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => { - const [allDay, setAllDay] = useState(false); - + const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked)); const handleAllDayChange = (e) => { const updatedSlots = slots.map(slot => ({ ...slot, @@ -303,7 +378,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o })); setSlots(updatedSlots); setAllDay(e.target.checked) - setCanUpdate(true); + // setCanUpdate(slots.some(slot => slot.isChecked)); + const anyChecked = updatedSlots.some(slot => slot.isChecked); + setCanUpdate(anyChecked); console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots)); }; useEffect(() => { @@ -358,9 +435,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return (
-
)} @@ -388,7 +403,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { // orange-500 from Tailwind CSS backgroundColor = '#f56565'; } - if (event.isactive) { + if (event.isActive) { switch (event.type) { case 'assignment': backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS diff --git a/components/cartevent/CartEventForm.tsx b/components/cartevent/CartEventForm.tsx index 1399799..d6d6fdd 100644 --- a/components/cartevent/CartEventForm.tsx +++ b/components/cartevent/CartEventForm.tsx @@ -15,7 +15,7 @@ model CartEvent { shiftDuration Int shifts Shift[] dayofweek DayOfWeek - isactive Boolean @default(true) + isActive Boolean @default(true) }*/ interface Location { id: number; @@ -174,8 +174,8 @@ export default function CartEventForm(props: IProps) {
- - + +
diff --git a/components/location/LocationCard.js b/components/location/LocationCard.js index 2a2a3ec..adaf15c 100644 --- a/components/location/LocationCard.js +++ b/components/location/LocationCard.js @@ -28,11 +28,11 @@ export default function LocationCard({ location }) { <>
router.push(`/cart/locations/edit/${location.id}`)} >
- {location.name} ({location.isactive ? "active" : "inactive"}) + {location.name} ({location.isActive ? "active" : "inactive"})

{location.address} diff --git a/components/location/LocationForm.js b/components/location/LocationForm.js index 098186a..4f58fdb 100644 --- a/components/location/LocationForm.js +++ b/components/location/LocationForm.js @@ -19,7 +19,7 @@ const common = require('src/helpers/common'); // id Int @id @default(autoincrement()) // name String // address String -// isactive Boolean @default(true) +// isActive Boolean @default(true) // content String? @db.Text // cartEvents CartEvent[] // reports Report[] @@ -65,7 +65,7 @@ export default function LocationForm() { const [location, set] = useState({ name: "", address: "", - isactive: true, + isActive: true, }); // const [isEdit, setIsEdit] = useState(false); @@ -171,11 +171,11 @@ export default function LocationForm() {

- {/* UI for Location.isactive */} + {/* UI for Location.isActive */}
- - + +
{/* backupLocation */} diff --git a/components/publisher/PublisherCard.js b/components/publisher/PublisherCard.js index a993240..248f067 100644 --- a/components/publisher/PublisherCard.js +++ b/components/publisher/PublisherCard.js @@ -57,7 +57,7 @@ export default function PublisherCard({ publisher }) { return isCardVisible ? ( // className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
- {publisher.firstName} {publisher.lastName} ({publisher.isactive ? "active" : "inactive"}) + {publisher.firstName} {publisher.lastName} ({publisher.isActive ? "active" : "inactive"})

{publisher.assignments.length} смени общо

diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js index f5f1f44..99bde59 100644 --- a/components/publisher/PublisherForm.js +++ b/components/publisher/PublisherForm.js @@ -23,7 +23,7 @@ import { UserRole } from "@prisma/client"; // lastName String // email String @unique // phone String? -// isactive Boolean @default(true) +// isActive Boolean @default(true) // isImported Boolean @default(false) // age Int? // availabilities Availability[] @@ -75,7 +75,7 @@ export default function PublisherForm({ item, me }) { }, []); const [publisher, set] = useState(item || { - isactive: true, + isActive: true, }); const handleChange = ({ target }) => { @@ -185,10 +185,13 @@ export default function PublisherForm({ item, me }) { -
- - + +
+
+ + +
@@ -232,60 +235,68 @@ export default function PublisherForm({ item, me }) {
-
- - -
-
- - -
-
- - +
+
+ + +
+
+
+ + + +
- -
- - -
-
-
- - - - - - + + {/* ADMINISTRATORS ONLY */} + +
+
+ +
+
+ + +
+
+ + +
+
+
+ + + + + + +
+
+
+ + +
+
+ Телеграм + Телеграм +
-
- - -
- - Телеграм - Телеграм - {/* ---------------------------- Actions --------------------------------- */}
@@ -312,7 +323,7 @@ export default function PublisherForm({ item, me }) {
-
+
) diff --git a/components/publisher/PublisherInlineForm.js b/components/publisher/PublisherInlineForm.js index 1956a61..b231520 100644 --- a/components/publisher/PublisherInlineForm.js +++ b/components/publisher/PublisherInlineForm.js @@ -4,8 +4,7 @@ import toast from "react-hot-toast"; import { useRouter } from "next/router"; const PublisherInlineForm = ({ publisherId, initialShiftsPerMonth }) => { - const [desiredShiftsPerMonth, setDesiredShiftsPerMonth] = useState(initialShiftsPerMonth); - const router = useRouter(); + const [desiredShiftsPerMonth, setDesiredShiftsPerMonth] = useState(initialShiftsPerMonth || 2); const storedValue = useRef(initialShiftsPerMonth); useEffect(() => { diff --git a/components/publisher/PublisherSearchBox.js b/components/publisher/PublisherSearchBox.js index 605b70c..4926504 100644 --- a/components/publisher/PublisherSearchBox.js +++ b/components/publisher/PublisherSearchBox.js @@ -18,7 +18,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS const fetchPublishers = async () => { console.log("fetchPublishers called"); try { - let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isactive&searchText=${searchText}&availabilities=false`; + let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isActive&searchText=${searchText}&availabilities=false`; if (filterDate) { url += `&filterDate=${common.getISODateOnly(filterDate)}`; @@ -29,7 +29,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS const { data: publishersData } = await axiosInstance.get(url); //setPublishers(publishersData); - const activePublishers = publishersData.filter(publisher => publisher.isactive === true); + const activePublishers = publishersData.filter(publisher => publisher.isActive === true); setPublishers(activePublishers); } catch (error) { diff --git a/components/publisher/ShiftsList.tsx b/components/publisher/ShiftsList.tsx index 49cc71a..ac3bb30 100644 --- a/components/publisher/ShiftsList.tsx +++ b/components/publisher/ShiftsList.tsx @@ -35,7 +35,7 @@ const ShiftsList = ({ assignments, selectedtab }: ShiftsListProps) => { try { var assignment = (await axiosInstance.get("/api/data/assignments/" + id)).data; assignment.isConfirmed = false; - assignment.isTentative = true; + // ! assignment.isTentative = true; // assignment.isDeleted = true; await axiosInstance.put("/api/data/assignments/" + id, assignment); toast.success("Shift Tentative", { diff --git a/components/reports/ExperienceForm.js b/components/reports/ExperienceForm.js index 43c230a..eff676f 100644 --- a/components/reports/ExperienceForm.js +++ b/components/reports/ExperienceForm.js @@ -102,7 +102,7 @@ export default function ExperienceForm({ publisherId, assgnmentId, existingItem, try { const response = await axiosInstance.post('/api/data/reports', item); console.log(response); - toast.success("Случката е записана. Благодарим Ви!"); + toast.success("Случката е записана. Благодаря!"); setTimeout(() => { if (onDone) { onDone(); diff --git a/components/reports/FeedbackForm.js b/components/reports/FeedbackForm.js index 28e939f..3cd0664 100644 --- a/components/reports/FeedbackForm.js +++ b/components/reports/FeedbackForm.js @@ -78,7 +78,7 @@ export default function FeedbackForm({ publisherId, onDone }) { try { const response = await axiosInstance.post('/api/data/reports', item); console.log(response); - toast.success("Благодарим Ви за вашия отзив!"); + toast.success("Благодаря за отзива!"); setTimeout(() => { if (onDone) { onDone(); diff --git a/components/reports/ReportForm.js b/components/reports/ReportForm.js index ab5bf75..b6f22b5 100644 --- a/components/reports/ReportForm.js +++ b/components/reports/ReportForm.js @@ -101,7 +101,7 @@ export default function ReportForm({ shiftId, existingItem, onDone }) { try { const response = await axiosInstance.post('/api/data/reports', item); console.log(response); - toast.success("Гоово. Благодарим Ви за отчета!"); + toast.success("Гоово. Благодаря за отчета!"); setTimeout(() => { if (onDone) { onDone(); diff --git a/components/sidebar.tsx b/components/sidebar.tsx index b0d4838..b20a168 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -99,7 +99,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) { try { const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed const locationsData = response.data - .filter(location => location.isactive === true) + .filter(location => location.isActive === true) .map(location => ({ text: location.name, url: `/cart/locations/${location.id}`, diff --git a/next.config.js b/next.config.js index 2bc690b..32d4f12 100644 --- a/next.config.js +++ b/next.config.js @@ -12,7 +12,7 @@ module.exports = { pageExtensions: ['ts', 'tsx', 'md', 'mdx'], // Replace `jsx?` with `tsx?` env: { env: process.env.NODE_ENV, - server: 'http://' + process.env.NEXT_PUBLIC_HOST + ':' + process.env.NEXT_PUBLIC_PORT + '', + server: process.env.NEXT_PUBLIC_PUBLIC_URL }, webpack(config, { isServer }) { diff --git a/package-lock.json b/package-lock.json index 21a5428..c876ac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pwwa", - "version": "0.9.9", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pwwa", - "version": "0.9.9", + "version": "1.1.2", "dependencies": { "@auth/prisma-adapter": "^1.4.0", "@emotion/react": "^11.11.3", @@ -16,7 +16,7 @@ "@mui/material": "^5.15.10", "@mui/x-date-pickers": "^6.19.4", "@premieroctet/next-crud": "^3.0.0", - "@prisma/client": "^5.10.2", + "@prisma/client": "^5.11.0", "@react-pdf/renderer": "^3.3.8", "@tailwindcss/forms": "^0.5.7", "@types/multer": "^1.4.11", @@ -80,6 +80,7 @@ "tailwindcss": "^3.4.1", "tw-elements": "^1.1.0", "typescript": "^5", + "uuid": "^9.0.1", "webpack-bundle-analyzer": "^4.10.1", "winston": "^3.11.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz", @@ -90,7 +91,7 @@ "devDependencies": { "cross-env": "^7.0.3", "depcheck": "^1.4.7", - "prisma": "^5.10.2" + "prisma": "^5.11.0" } }, "node_modules/@alloc/quick-lru": { @@ -2282,9 +2283,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.10.2.tgz", - "integrity": "sha512-ef49hzB2yJZCvM5gFHMxSFL9KYrIP9udpT5rYo0CsHD4P9IKj473MbhU1gjKKftiwWBTIyrt9jukprzZXazyag==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.11.0.tgz", + "integrity": "sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -2299,39 +2300,39 @@ } }, "node_modules/@prisma/debug": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.2.tgz", - "integrity": "sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz", + "integrity": "sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.2.tgz", - "integrity": "sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.11.0.tgz", + "integrity": "sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.10.2", - "@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", - "@prisma/fetch-engine": "5.10.2", - "@prisma/get-platform": "5.10.2" + "@prisma/debug": "5.11.0", + "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "@prisma/fetch-engine": "5.11.0", + "@prisma/get-platform": "5.11.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz", - "integrity": "sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==", + "version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz", + "integrity": "sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz", - "integrity": "sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz", + "integrity": "sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.10.2", - "@prisma/engines-version": "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9", - "@prisma/get-platform": "5.10.2" + "@prisma/debug": "5.11.0", + "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "@prisma/get-platform": "5.11.0" } }, "node_modules/@prisma/generator-helper": { @@ -2348,12 +2349,12 @@ "integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==" }, "node_modules/@prisma/get-platform": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.2.tgz", - "integrity": "sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.11.0.tgz", + "integrity": "sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.10.2" + "@prisma/debug": "5.11.0" } }, "node_modules/@prisma/internals": { @@ -9294,13 +9295,13 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.10.2.tgz", - "integrity": "sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.11.0.tgz", + "integrity": "sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.10.2" + "@prisma/engines": "5.11.0" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index b063542..370ffd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pwwa", - "version": "0.9.9", + "version": "1.1.2", "private": true, "description": "JW PW Web App", "repository": "http://git.d-popov.com/popov/next-cart-app.git", @@ -9,15 +9,15 @@ }, "homepage": "https://git.d-popov.com/popov/next-cart-app", "scripts": { - "debug": "nodemon --inspect server.js", - "debug-env": "cross-env NODE_ENV=development dotenv -e .env.development -- nodemon --inspect server.js", + "debug": "node server.js", + "debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js", + "nodeenv": "dotenv -e .env.$APP_ENV -- node server.js", + "prod": "npx next build && dotenv -e .env.production -- node server.js", "build": "next build", "buildWin": "npm run build", "start": "next start", "devNext": "next dev --port 3003 --experimental-https", - "test": "dotenv -e .env.$NODE_ENV -- nodemon --inspect server.js", - "nodeenv": "dotenv -e .env.$NODE_ENV -- node server.js", - "prod": "npx next build && dotenv -e .env.production -- node server.js" + "test": "dotenv -e .env.$NODE_ENV -- nodemon --inspect server.js" }, "author": "Dobromir Popov ", "_moduleAliases": { @@ -33,7 +33,7 @@ "@mui/material": "^5.15.10", "@mui/x-date-pickers": "^6.19.4", "@premieroctet/next-crud": "^3.0.0", - "@prisma/client": "^5.10.2", + "@prisma/client": "^5.11.0", "@react-pdf/renderer": "^3.3.8", "@tailwindcss/forms": "^0.5.7", "@types/multer": "^1.4.11", @@ -97,6 +97,7 @@ "tailwindcss": "^3.4.1", "tw-elements": "^1.1.0", "typescript": "^5", + "uuid": "^9.0.1", "webpack-bundle-analyzer": "^4.10.1", "winston": "^3.11.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz", @@ -107,6 +108,6 @@ "devDependencies": { "cross-env": "^7.0.3", "depcheck": "^1.4.7", - "prisma": "^5.10.2" + "prisma": "^5.11.0" } -} +} \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index e89920c..6538651 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -35,7 +35,7 @@ export default function App({ rel="stylesheet" /> - + diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 764362a..61d1ee4 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -24,12 +24,12 @@ import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshT export const authOptions: NextAuthOptions = { // https://next-auth.js.org/configuration/providers/oauth - site: process.env.NEXTAUTH_URL, + site: process.env.NEXT_PUBLIC_PUBLIC_URL, secret: process.env.NEXTAUTH_SECRET, // Ensure you have this set in your .env file //adapter: PrismaAdapter(prisma), providers: [ // register new URL at https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 - //Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwhitnessing.com/ + //Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwitnessingmwitnessing.com/ GoogleProvider({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET, @@ -41,11 +41,15 @@ export const authOptions: NextAuthOptions = { } } }), - AzureADProvider({ - clientId: process.env.AZURE_AD_CLIENT_ID, - clientSecret: process.env.AZURE_AD_CLIENT_SECRET, - tenantId: process.env.AZURE_AD_TENANT_ID, + AppleProvider({ + clientId: process.env.APPLE_ID, + clientSecret: process.env.APPLE_SECRET }), + // AzureADProvider({ + // clientId: process.env.AZURE_AD_CLIENT_ID, + // clientSecret: process.env.AZURE_AD_CLIENT_SECRET, + // tenantId: process.env.AZURE_AD_TENANT_ID, + // }), CredentialsProvider({ // The name to display on the sign in form (e.g. 'Sign in with...') name: 'Credentials', @@ -87,11 +91,7 @@ export const authOptions: NextAuthOptions = { return null; } - }), - // AppleProvider({ - // clientId: process.env.APPLE_ID, - // clientSecret: process.env.APPLE_SECRET - // }) + }) /* EmailProvider({ server: { diff --git a/pages/api/email.ts b/pages/api/email.ts new file mode 100644 index 0000000..4b23b98 --- /dev/null +++ b/pages/api/email.ts @@ -0,0 +1,302 @@ +// API endpoint to process email user actions - urls we send in emails to users + +import { getToken } from "next-auth/jwt"; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter, expressWrapper } from "next-connect"; +const common = require('../../src/helpers/common'); +const emailHelper = require('../../src/helpers/email'); +const { v4: uuidv4 } = require('uuid'); +const CON = require("../../src/helpers/const"); + +import fs from 'fs'; +import path from 'path'; +const handlebars = require("handlebars"); + +const router = createRouter(); + + +//action to accept coverme request from email + + +/** + * + * @param req import { NextApiRequest, NextApiResponse } from 'next' + * @param res import { NextApiRequest, NextApiResponse } from 'next' + */ +export default async function handler(req, res) { + const prisma = common.getPrismaClient(); + + const action = req.query.action; + const emailaction = req.query.emailaction; + // Retrieve and validate the JWT token + + //response is a special action that does not require a token + if (action == "email_response") { + switch (emailaction) { + case "coverMeAccept": + //validate shiftId and assignmentId + let shiftId = req.query.shiftId; + let userId = req.query.userId; + let publisher = await prisma.publisher.findUnique({ + where: { + id: userId + } + }); + // Update the user status to accepted + console.log("User: " + publisher.firstName + " " + publisher.lastName + " accepted the CoverMe request"); + + let assignmentPID = req.query.assignmentPID; + if (!shiftId) { + return res.status(400).json({ message: "Shift ID is not provided" }); + } + if (!assignmentPID) { + return res.status(400).json({ message: "Assignment PID is not provided" }); + } + //check if the assignment request is still open + const assignment = await prisma.assignment.findFirst({ + where: { + publicGuid: assignmentPID, + shiftId: parseInt(shiftId), + isConfirmed: false + }, + include: { + shift: { + include: { + cartEvent: { + include: { + location: true + } + }, + assignments: { + include: { + publisher: true + // { + // include: { + // email: true, + // firstName: true, + // lastName: true + // } + // } + } + } + } + }, + publisher: true + } + }); + + if (!assignment) { + const messagePageUrl = `/message?message=${encodeURIComponent('Някой друг вече е отговорил на рази заявка за заместване')}&type=info&caption=${encodeURIComponent('Заявката е вече обработена')}`; + res.redirect(messagePageUrl); + return; + } + + let to = assignment.shift.assignments.map(a => a.publisher.email); + to.push(publisher.email); + + // update the assignment. clear the guid, isConfirmed to true + await prisma.assignment.update({ + where: { + id: assignment.id + }, + data: { + publisherId: userId, + publicGuid: null, // if this exists, we consider the request open + isConfirmed: true + } + }); + const newAssignment = await prisma.assignment.findFirst({ + where: { + shiftId: parseInt(shiftId), + isConfirmed: true + }, + include: { + shift: { + include: { + cartEvent: { + include: { + location: true + } + }, + assignments: { + include: { + publisher: true + } + } + } + } + } + }); + + + const shiftStr = `${CON.weekdaysBG[assignment.shift.startTime.getDay()]} ${CON.GetDateFormat(assignment.shift.startTime)} at ${assignment.shift.cartEvent.location.name} from ${CON.GetTimeFormat(assignment.shift.startTime)} to ${CON.GetTimeFormat(assignment.shift.endTime)}`; + + const newPubs = newAssignment.shift.assignments.map(a => ({ + name: `${a.publisher.firstName} ${a.publisher.lastName}`, + phone: a.publisher.phone + })); + + let model = { + user: publisher, + shiftStr: shiftStr, + shiftId: assignment.shiftId, + prefix: publisher.isMale ? "Брат" : "Сестра", + oldPubName: assignment.publisher.firstName + " " + assignment.publisher.lastName, + firstName: publisher.firstName, + lastName: publisher.lastName, + newPubs: newPubs, + placeName: assignment.shift.cartEvent.location.name, + dateStr: common.getDateFormated(assignment.shift.startTime), + time: common.formatTimeHHmm(assignment.shift.startTime), + sentDate: common.getDateFormated(new Date()) + }; + + emailHelper.SendEmailHandlebars(to, "coverMeAccepted", model); + + const messagePageUrl = `/message?message=${encodeURIComponent('Вашата заявка за замстване е обработена успешно')}&type=info&caption=${encodeURIComponent('Благодаря!')}`; + res.redirect(messagePageUrl); + + break; + + //POST + case "send_report": //we can send report form in the emails to the user. process the POSTED data here + // Send report form to the user + //get from POST data: locationId, date, placementCount, videoCount, returnVisitInfoCount, conversationCount + let locationId = req.body.locationId; + let date = req.body.date; + let placementCount = req.body.placementCount; + let videoCount = req.body.videoCount; + let returnVisitInfoCount = req.body.returnVisitInfoCount; + let conversationCount = req.body.conversationCount; + + console.log("User: " + user.email + " sent a report: " + + locationId + " " + date + " " + + placementCount + " " + videoCount + " " + + returnVisitInfoCount + " " + conversationCount); + + //save the report in the database + await prisma.report.create({ + data: { + userId: parseInt(userId), + locationId: parseInt(locationId), + date: date, + placementCount: parseInt(placementCount), + videoCount: parseInt(videoCount), + returnVisitInfoCount: parseInt(returnVisitInfoCount), + conversationCount: parseInt(conversationCount) + } + }); + + break; + } + // //send email response to the user + // const emailResponse = await common.sendEmail(user.email, "Email Action Processed", + // "Your email action was processed successfully"); + } + else { + + const token = await getToken({ req: req }); + if (!token) { + // If no token or invalid token, return unauthorized status + return res.status(401).json({ message: "Unauthorized to call this API endpoint" }); + } + + const user = await prisma.publisher.findUnique({ + where: { + email: token.email + } + }); + + switch (action) { + case "sendCoverMeRequestByEmail": + // Send CoverMe request to the user + //get from POST data: shiftId, assignmentId, date + //let shiftId = req.body.shiftId; + let assignmentId = req.body.assignmentId; + let date = req.body.date; + + console.log("User: " + user.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " " + date); + + let assignment = await prisma.assignment.findUnique({ + where: { + id: parseInt(assignmentId) + }, + include: { + shift: { + include: { + cartEvent: { + include: { + location: true + } + } + } + } + } + }); + + // update the assignment. generate new publicGuid, isConfirmed to false + let newPublicGuid = uuidv4(); + await prisma.assignment.update({ + where: { + id: parseInt(assignmentId) + }, + data: { + publicGuid: newPublicGuid, // if this exists, we consider the request open + isConfirmed: false + } + }); + + //get all subscribed publisers + const subscribedPublishers = await prisma.publisher.findMany({ + where: { + isSubscribedToCoverMe: true + } + }); + //send email to all subscribed publishers + for (let i = 0; i < subscribedPublishers.length; i++) { + if (subscribedPublishers[i].id == user.id) { + continue; + } + + //send email to subscribed publisher + let acceptUrl = process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=coverMeAccept&userId=" + subscribedPublishers[i].id + "&shiftId=" + assignment.shiftId + "&assignmentPID=" + newPublicGuid; + + let model = { + user: user, + shiftId: assignment.shiftId, + acceptUrl: acceptUrl, + prefix: user.isMale ? "Брат" : "Сестра", + firstName: subscribedPublishers[i].firstName, + lastName: subscribedPublishers[i].lastName, + email: subscribedPublishers[i].email, + placeName: assignment.shift.cartEvent.location.name, + dateStr: common.getDateFormated(assignment.shift.startTime), + time: common.formatTimeHHmm(assignment.shift.startTime), + sentDate: common.getDateFormated(new Date()) + }; + let results = emailHelper.SendEmailHandlebars( + { + name: subscribedPublishers[i].firstName + " " + subscribedPublishers[i].lastName, + email: subscribedPublishers[i].email + }, "coverMe", model); + // if (results) { + // console.log("Error sending email: " + error); + // return res.status(500).json({ message: "Error sending email:" + error }); + //} + + if (results) { + console.log("Email sent to: " + subscribedPublishers[i].email); + } + + } + break; + default: + return res.status(400).json({ message: "Invalid action" }); + } + + return res.status(200).json({ message: "User action processed" }); + } + +} +router.use(expressWrapper(handler)); + diff --git a/pages/api/index.ts b/pages/api/index.ts index 563bda8..afef4b1 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -1,12 +1,13 @@ import { getToken } from "next-auth/jwt"; import { NextApiRequest, NextApiResponse } from 'next' -import { DayOfWeek } from '@prisma/client'; +import { DayOfWeek, AvailabilityType } from '@prisma/client'; const common = require('../../src/helpers/common'); const data = require('../../src/helpers/data'); const subq = require('../../prisma/bl/subqueries'); import fs from 'fs'; import path from 'path'; +import { all } from "axios"; /** * @@ -29,7 +30,7 @@ export default async function handler(req, res) { var action = req.query.action; var filter = req.query.filter; - let date: Date; + let date: Date, monthInfo: any; if (req.query.date) { date = new Date(req.query.date); //date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead) @@ -77,7 +78,7 @@ export default async function handler(req, res) { //gets publisher by names with availabilities and assignments case "deleteAvailabilityForPublisher": let publisherId = req.query.publisherId; - let dateFor, monthInfo; + let dateFor; if (req.query.date) { dateFor = new Date(req.query.date); //get month info from date @@ -143,7 +144,7 @@ export default async function handler(req, res) { case "getUnassignedPublishers": //let monthInfo = common.getMonthDatesInfo(date); - let allPubs = await filterPublishers("id,firstName,lastName,email,isactive".split(","), "", date, true, true, false); + let allPubs = await filterPublishers("id,firstName,lastName,email,isActive".split(","), "", date, true, true, false); let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0); res.status(200).json(unassignedPubs); break; @@ -209,7 +210,134 @@ export default async function handler(req, res) { res.status(200).json(shiftsForDate); break; + case "copyOldAvailabilities": + //get all publishers that don't have availabilities for the current month + monthInfo = common.getMonthDatesInfo(date); + // await prisma.availability.deleteMany({ + // where: { + // startTime: { + // gte: monthInfo.firstMonday, + // }, + // isFromPreviousMonth: true + // } + // }); + let outdatedPubs = await prisma.publisher.findMany({ + where: { + availabilities: { + none: { + startTime: { + gte: monthInfo.firstMonday, + } + } + } + }, + select: { + id: true, + firstName: true, + lastName: true, + availabilities: true + } + }); + outdatedPubs.forEach(async pub => { + // avail.startTime >= monthInfo.firstMonday + //get prev month date: + let prevMonth = new Date(monthInfo.firstMonday); + prevMonth.setMonth(prevMonth.getMonth() - 1); + let prevMonthInfo = common.getMonthDatesInfo(prevMonth); + pub.availabilities = pub.availabilities.filter(avail => avail.startTime > prevMonthInfo.firstMonday); + console.log("" + pub.firstName + " " + pub.lastName + " copying " + pub.availabilities.length + " availabilities from previous months."); + pub.availabilities.forEach(async avail => { + //get the new date based on the day of week and week of month + if (!avail.weekOfMonth) { + avail.weekOfMonth = common.getWeekOfMonth(avail.startTime) + } + let origMonthInfo = common.getMonthDatesInfo(avail.startTime); + let newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, avail.weekOfMonth, avail.dayofweek, avail.startTime); + //ToDo: fix double check. also check if we're in 5th week and the month has 4 weeks + // const availability = await data.findPublisherAvailability(publisher.id, newStart); + // if (availability) { + // return; + // } + let newEnd = new Date(newStart.getTime()); + newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0); + let data = { + publisherId: pub.id, + dayOfMonth: null, + dayofweek: avail.dayofweek || common.getDayOfWeekNameEnEnumForDate(avail.startTime), + weekOfMonth: avail.weekofMonth || common.getWeekOfMonth(avail.startTime), + // null for auto generated availabilities + //dateOfEntry: new Date(), //avail.dateOfEntry || avail.startTime, + startTime: newStart, + endTime: newEnd, + type: AvailabilityType.Monthly, + isFromPreviousMonth: true, + name: avail.name || "старо предпочитание", + // parentAvailabilityId: avail.id + parentAvailability: { + connect: { + id: avail.id + } + } + } + await prisma.availability.create({ data: data }); + + //if month has 5 weeks and the monthInfo has 4 weeks copy the availabilities also from the 1st week to the 5th week + if (monthInfo.nrOfWeeks == 5 && avail.weekOfMonth == 1 && origMonthInfo.nrOfWeeks == 4) { + newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, 5, avail.dayofweek, avail.startTime); + newEnd = new Date(newStart.getTime()); + newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0); + data.weekOfMonth = 5; + data.startTime = newStart; + data.endTime = newEnd; + await prisma.availability.create({ data: data }); + } + + }); + }); + + //convert old assignments to availabilities + + res.status(200).json({ "message": "ok" }); + break; + case "deleteCopiedAvailabilities": + //delete all availabilities that are copied from previous months + monthInfo = common.getMonthDatesInfo(date); + await prisma.availability.deleteMany({ + where: { + startTime: { + gte: monthInfo.firstMonday, + }, + isFromPreviousMonth: true + } + }); + + case "replaceInAssignment": + const { oldPublisherId, newPublisherId, shiftId } = req.method === "POST" ? req.body : req.query; + + const result = await replaceInAssignment(oldPublisherId, newPublisherId, shiftId); + res.status(200).json(result); + break; + + case "updateShifts": + //get all shifts for the month and publish them (we pass date ) + let monthInfo = common.getMonthDatesInfo(date); + let isPublished = common.parseBool(req.query.isPublished); + let updated = await prisma.shift.updateMany({ + where: { + startTime: { + gte: new Date(monthInfo.firstMonday.getFullYear(), monthInfo.firstMonday.getMonth(), 1), + lt: new Date(monthInfo.lastSunday.getFullYear(), monthInfo.lastSunday.getMonth() + 1, 1), + } + }, + data: { + isPublished: isPublished + } + }); + console.log("Updated shifts: " + updated.count); + res.status(200).json({ "message": "ok" }); + + break; default: res.status(200).json({ @@ -224,6 +352,7 @@ export default async function handler(req, res) { } + export async function getMonthlyStatistics(selectFields, filterDate) { let publishers = []; @@ -406,7 +535,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet let dayOfWeekEnum: DayOfWeek if (filterDate) { // Determine day of week using common function - dayOfWeekEnum = common.getDayOfWeekNameEnEnum(filterDate); + dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate); if (filterDate.getHours() > 21 || filterDate.getHours() < 6) { filterDate.setHours(0, 0, 0, 0); // Set to midnight } @@ -435,8 +564,8 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet // also, permanent weekly availabilities will have dayOfMonth = null and type = 0 // for 0 we will match by dayOfWeekEnum and times // for 1 we will match by exact date and times - // for 2 we will match by dayofweek, weeknr and times - // for 3 we will match by dayofweek, weeknr and times - this is the same as 2, but we will not count them as availabilities for the current month + // for 2 we will match by dayofweek, weekOfMonth and times + // for 3 we will match by dayofweek, weekOfMonth and times - this is the same as 2, but we will not count them as availabilities for the current month // generaion of schedule: @@ -451,6 +580,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet //substract the time difference between from ISO string and local time const offset = filterDate.getTimezoneOffset() * 60000; // offset in milliseconds var dateAsISO = new Date(filterDate.getTime() + offset); + //if full day, match by date only if (filterDate.getHours() == 0 || dateAsISO.getHours() == 0) { whereClause["availabilities"] = { some: { @@ -464,16 +594,26 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) // This includes availabilities from previous assignments but not with preference { - dayOfMonth: null, + dayOfMonth: null, // includes monthly and weekly repeats dayofweek: dayOfWeekEnum, - // ToDo: and weekNr - //startTime: { gte: currentMonthStart }, + // ToDo: and weekOfMonth + startTime: { lte: filterDate }, + AND: [ + { + OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever + { endDate: { gte: filterDate } }, + { endDate: null } + ] + } + ] } ] } }; } + //if not full day, match by date and time else { + //match exact time (should be same as data.findPublisherAvailability()) whereClause["availabilities"] = { some: { OR: [ @@ -487,12 +627,14 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet { dayOfMonth: null, dayofweek: dayOfWeekEnum, + startTime: { gte: filterDate }, } ] } }; } - } else { // we use month filter if date is passed and useDateFilter is false + } else { + // we use month filter if date is passed and useDateFilter is false to get all publishers with availabilities for the current month if (fetchAvailabilities) { // If no filter date, return all publishers's availabilities for currentMonthStart whereClause["availabilities"] = { @@ -518,7 +660,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet // dayOfMonth: true, // startTime: true, // endTime: true, - // weekNr: true, + // weekOfMonth: true, // type: true // }, // where: { @@ -609,6 +751,11 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd; }); + //if pub has availabilities for the current day + pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => { + return avail.startTime >= filterDate && avail.startTime <= filterDateEnd; + }); + }); if (filterDate && useDateFilter) { @@ -670,4 +817,22 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig } } return result; +} + + +async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) { + const prisma = common.getPrismaClient(); + const result = await prisma.assignment.updateMany({ + where: { + publisherId: oldPublisherId, + shiftId: shiftId + }, + data: { + publisherId: newPublisherId, + isConfirmed: false, + isBySystem: true, + isMailSent: false + } + }); + return result; } \ No newline at end of file diff --git a/pages/api/schedule.ts b/pages/api/schedule.ts index d94d567..087ce81 100644 --- a/pages/api/schedule.ts +++ b/pages/api/schedule.ts @@ -89,12 +89,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const shifts = await prisma.shift.findMany({ where: { - isactive: true, + isActive: true, + isPublished: true, startTime: { gte: fromDate, lt: toDate, }, }, + orderBy: { + startTime: 'asc', + }, include: { assignments: { where: {}, @@ -147,7 +151,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) .join(", "), }; - groupedShifts[day][time].push(shiftSchedule); + if (shiftSchedule.names.length > 0) { + groupedShifts[day][time].push(shiftSchedule); + } } } catch (err) { console.log(err + " " + JSON.stringify(shifts[i])); @@ -188,8 +194,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) dayEvent.shifts.push(...groupedShifts[day][time]); } - - monthlySchedule.events.push(dayEvent); + if (dayEvent) { + monthlySchedule.events.push(dayEvent); + } } const outputPath = path.join(process.cwd(), 'public', 'content', 'output'); diff --git a/pages/api/shiftgenerate.ts b/pages/api/shiftgenerate.ts index ba61fb8..f0137f6 100644 --- a/pages/api/shiftgenerate.ts +++ b/pages/api/shiftgenerate.ts @@ -68,7 +68,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { case "test": var data = prisma.shift.findMany({ where: { - isactive: true + isActive: true } }); @@ -110,7 +110,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont } console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")"); - const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":{"$eq":true}}`); + const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":{"$eq":true}}`); //// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo); //use filterPublishers from /pages/api/data/index.ts to get publishers with stats @@ -178,7 +178,8 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont shiftNr++; const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0"); shiftAssignments = []; - console.log("[shift " + shiftNr + "] " + __shiftName); + let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime(); + console.log("[shift " + shiftNr + "] " + __shiftName + ", transport: " + (isTransportRequired ? "yes" : "no") + ", " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString() + " (end time: " + endTime.toLocaleTimeString() + ", " + event.shiftDuration + " min)"); if (autoFill || copyFromPreviousMonth) { // ########################################### @@ -307,7 +308,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont assignments: 'true', availabilities: 'true', date: common.getISODateOnly(shiftStart), - select: 'id,firstName,lastName,isactive,desiredShiftsPerMonth' + select: 'id,firstName,lastName,isActive,desiredShiftsPerMonth' }); let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data; let availablePublishers = allAvailablePublishers; @@ -395,6 +396,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont startTime: shiftStart, endTime: shiftEnd, name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(), + requiresTransport: isTransportRequired, cartEvent: { connect: { id: event.id, @@ -442,7 +444,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont } //create shifts using API - // const { data: createdShifts } = await axios.post(`${process.env.NEXTAUTH_URL}/api/data/shifts`, shiftsToCreate); + // const { data: createdShifts } = await axios.post(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/shifts`, shiftsToCreate); //const { data: allshifts } = await axios.get(`/api/data/shifts`); return {}; //allshifts; @@ -558,7 +560,7 @@ async function DeleteSchedule(axios: Axios, date: Date, forDay: Boolean | undefi async function CreateCalendarForUser(eventId: string | string[] | undefined) { try { - CAL.authorizeNew(); + //CAL.authorizeNew(); CAL.createEvent(eventId); } catch (error) { console.log(error); @@ -614,12 +616,12 @@ async function ImportShiftsFromDocx(axios: Axios) { // prisma.publisher.findMany({ // where: { -// isactive: true, +// isActive: true, // }, // include: { // availabilities: { // where: { -// isactive: true, +// isActive: true, // }, // }, // assignments: { diff --git a/pages/cart/availabilities/index.tsx b/pages/cart/availabilities/index.tsx index 4cd257b..2f44dc3 100644 --- a/pages/cart/availabilities/index.tsx +++ b/pages/cart/availabilities/index.tsx @@ -30,7 +30,7 @@ export default function AvPage({ initialItems, id }: IProps) { date: new Date(item.startTime), start: new Date(item.startTime), end: new Date(item.endTime), - isactive: item.isactive, + isActive: item.isActive, publisherId: item.publisher.id, dayOfMonth: item.dayOfMonth, dayOfWeek: item.dayOfWeek, @@ -66,9 +66,9 @@ export default function AvPage({ initialItems, id }: IProps) { {initialItems?.map((item: Availability) => ( - + - {item.id} {item.isactive} + {item.id} {item.isActive} {item.publisher.lastName}, {item.publisher.firstName} @@ -133,19 +133,19 @@ export const getServerSideProps = async (context) => { const role = session?.user.role; console.log("server role: " + role); - var queryUrl = process.env.NEXTAUTH_URL + "/api/data/availabilities?select=id,name,isactive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id"; + var queryUrl = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities?select=id,name,isActive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id"; if (role === UserRole.USER || context.query.my) { queryUrl += `&where={"publisherId":"${session?.user.id}"}`; } else if (role == UserRole.ADMIN) { if (context.query.id) { queryUrl += `&where={"publisherId":"${context.query.id}"}`; } else { - queryUrl += `&where={"isactive":true}`; + queryUrl += `&where={"isActive":true}`; } } var resp = await axios.get( queryUrl - // process.env.NEXTAUTH_URL + "/api/data/availabilities?include=publisher", + // process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities?include=publisher", , { decompress: true }); var items = resp.data; console.log("got " + items.length + " availabilities"); diff --git a/pages/cart/availabilities/new.tsx b/pages/cart/availabilities/new.tsx index f281459..06a91b7 100644 --- a/pages/cart/availabilities/new.tsx +++ b/pages/cart/availabilities/new.tsx @@ -31,7 +31,7 @@ export const getServerSideProps = async (context) => { }; } const { data: item } = await axios.get( - process.env.NEXTAUTH_URL + "/api/data/availabilities/" + context.params.id + process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities/" + context.params.id ); return { diff --git a/pages/cart/calendar/index.tsx b/pages/cart/calendar/index.tsx index 244128c..5bb9269 100644 --- a/pages/cart/calendar/index.tsx +++ b/pages/cart/calendar/index.tsx @@ -14,6 +14,7 @@ const common = require('src/helpers/common'); import { toast } from 'react-toastify'; import ProtectedRoute from '../../../components/protectedRoute'; import ConfirmationModal from '../../../components/ConfirmationModal'; +import LocalShippingIcon from '@mui/icons-material/LocalShipping'; // import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons @@ -56,6 +57,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { const [allShifts, setAllShifts] = useState(initialShifts); + const [isPublished, setIsPublished] = useState(() => initialShifts.some(shift => shift.isPublished)); const [value, onChange] = useState(new Date()); const [shifts, setShifts] = React.useState([]); const [error, setError] = React.useState(null); @@ -99,7 +101,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date)); if (isCheckboxChecked) { console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`); - const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`); + const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); setAvailablePubs(availablePubsForDate); } else { @@ -107,7 +109,14 @@ export default function CalendarPage({ initialEvents, initialShifts }) { try { const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`); setShifts(shiftsForDate); - let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`); + setIsPublished(shiftsForDate.some(shift => shift.isPublished)); + let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); + + availablePubsForDate.forEach(pub => { + pub.canTransport = pub.availabilities.some(av => + av.isWithTransportIn || av.isWithTransportOut + ); + }); //remove availabilities that are isFromPreviousAssignment or from previous month for each publisher // availablePubsForDate = availablePubsForDate.map(pub => { // pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false); @@ -134,30 +143,39 @@ export default function CalendarPage({ initialEvents, initialShifts }) { const handleShiftSelection = (selectedShift) => { setSelectedShiftId(selectedShift.id); const updatedPubs = availablePubs.map(pub => { - const isAvailableForShift = pub.availabilities.some(avail => + const av = pub.availabilities?.find(avail => avail.startTime <= selectedShift.startTime && avail.endTime >= selectedShift.endTime - && avail.isFromPreviousAssignment == false ); + if (av) { + pub.isAvailableForShift = true; + pub.canTransport = av.isWithTransportIn || av.isWithTransportOut; + } + + // const isAvailableForShift = pub.availabilities.some(avail => + // avail.startTime <= selectedShift.startTime + // && avail.endTime >= selectedShift.endTime + // && avail.isFromPreviousAssignment == false + // ); const isAvailableForShiftWithPrevious = pub.availabilities.some(avail => avail.startTime <= selectedShift.startTime && avail.endTime >= selectedShift.endTime ); - //! console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`); - //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime)); - //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id))); + // //! console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`); + // //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime)); + // //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id))); - const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', '); - //! console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`); - return { ...pub, isAvailableForShift, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id }; + // const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', '); + // //! console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`); + return { ...pub, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id }; }); // Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments, // currentMonthAssignments and previousMonthAssignments properties // Sort publishers based on availability and then by assignment counts. const sortedPubs = updatedPubs.sort((a, b) => { - if (a.isactive !== b.isactive) { - return a.isactive ? -1 : 1; + if (a.isActive !== b.isActive) { + return a.isActive ? -1 : 1; } // First, sort by isselected. if (a.isSelected !== b.isSelected) { @@ -339,7 +357,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { const newAssignment = { publisher: { connect: { id: publisher.id } }, shift: { connect: { id: shiftId } }, - isactive: true, + isActive: true, isConfirmed: true }; const { data } = await axiosInstance.post("/api/data/assignments", newAssignment); @@ -495,10 +513,26 @@ export default function CalendarPage({ initialEvents, initialShifts }) { } } + const togglePublished = async () => { + try { + const publishState = !isPublished; // Toggle the state + const isPublishedParam = publishState ? 'true' : 'fasle'; + await axiosInstance.get(`/api/?action=updateShifts&isPublished=${isPublishedParam}&date=${common.getISODateOnly(value)}`); + setIsPublished(publishState); // Update state based on the action + + } catch (error) { + console.log(error); + } + } + const [isMenuOpen, setIsMenuOpen] = useState(false); const [isConfirmModalOpen, setConfirmModalOpen] = useState(false); + async function copyOldAvailabilities(event: MouseEvent): Promise { + await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`); + } + return ( <> @@ -534,6 +568,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) { }} message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?" /> +
+
)} @@ -653,7 +694,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { // Determine border class if selected const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : ''; // Determine opacity class - const activeOpacityClass = pub.isactive ? '' : 'opacity-25'; + const activeOpacityClass = pub.isActive ? '' : 'opacity-25'; return ( @@ -664,6 +705,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { > {pub.firstName} {pub.lastName} + {pub.canTransport && ()}
@@ -849,9 +891,9 @@ import axiosServer from '../../../src/axiosServer'; import { start } from 'repl'; export const getServerSideProps = async (context) => { const axios = await axiosServer(context); - const baseUrl = common.getBaseUrl(); - console.log('runtime BaseUrl: ' + baseUrl); - console.log('runtime NEXTAUTH_URL: ' + process.env.NEXTAUTH_URL); + // const baseUrl = common.getBaseUrl(); + // console.log('runtime BaseUrl: ' + baseUrl); + console.log('runtime NEXT_PUBLIC_PUBLIC_URL: ' + process.env.NEXT_PUBLIC_PUBLIC_URL); console.log('Runtime Axios Base URL:', axios.defaults.baseURL); const currentDate = new Date(); @@ -861,20 +903,20 @@ export const getServerSideProps = async (context) => { const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`; const prismaClient = common.getPrismaClient(); - // let events = await prismaClient.cartEvent.findMany({ where: { isactive: true } }); + // let events = await prismaClient.cartEvent.findMany({ where: { isActive: true } }); // events = events.map(event => ({ // ...event, // // Convert Date objects to ISO strings // startTime: event.startTime.toISOString(), // endTime: event.endTime.toISOString(), // })); - const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":true}`); + const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":true}`); //const { data: shifts } = await axios.get(url); // get all shifts for the month, including assigments let shifts = await prismaClient.shift.findMany({ where: { - isactive: true, + isActive: true, startTime: { gte: firstDayOfMonth, //lt: lastDayOfMonth diff --git a/pages/cart/cartevents/edit/[id].tsx b/pages/cart/cartevents/edit/[id].tsx index 81fca35..689c487 100644 --- a/pages/cart/cartevents/edit/[id].tsx +++ b/pages/cart/cartevents/edit/[id].tsx @@ -7,9 +7,9 @@ export const getServerSideProps = async (context) => { console.log("edit page getServerSideProps"); const axios = await axiosServer(context); const { id } = context.query; - const { data } = await axios.get(`${process.env.NEXTAUTH_URL}/api/data/cartevents/` + id); + const { data } = await axios.get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/cartevents/` + id); const locations = await axios - .get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`) + .get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations?select=id,name`) .then((res) => { console.log("locations: " + JSON.stringify(res.data)); return res.data; diff --git a/pages/cart/cartevents/index.tsx b/pages/cart/cartevents/index.tsx index c5006c1..0396d9f 100644 --- a/pages/cart/cartevents/index.tsx +++ b/pages/cart/cartevents/index.tsx @@ -69,7 +69,7 @@ export default function CartEventPage({ items, locations }: ICartEventPageProps) {item.shiftDuration} - {item.isactive ? "Yes" : "No"} + {item.isActive ? "Yes" : "No"} +
+ setIsModalOpen(false)} + forDate={new Date(assignment?.shift.startTime)} + useFilterDate={useFilterDate} + onUseFilterDateChange={(value) => setUseFilterDate(value)}> + + { + setIsConfirmModalOpen(true); + setNewPublisher(publisher); + }} + showAllAuto={true} + showSearch={true} + showList={false} + /> + + + { setIsConfirmModalOpen(false); setNewPublisher(null); }} + onConfirm={handleReplaceInAssignment} + message="Това действие ще те замести в назначената ти смяна. Потвърждаваш ли, че заместника знае за тази промяна." + />
); @@ -80,14 +183,18 @@ export const getServerSideProps = async (context) => { } const prisma = common.getPrismaClient(); - const publisher = await prisma.publisher.findMany({ + const monthInfo = common.getMonthInfo(new Date()); + //minus 1 day from the firstMonday to get the last Sunday + const lastSunday = new Date(monthInfo.firstMonday); + lastSunday.setDate(lastSunday.getDate() - 1); + const publisher = await prisma.publisher.findUnique({ where: { id: session.user.id, assignments: { some: { shift: { startTime: { - gte: new Date(), + gte: lastSunday, }, }, }, @@ -96,15 +203,29 @@ export const getServerSideProps = async (context) => { include: { assignments: { include: { - shift: true, + shift: { + include: { + assignments: { + include: { + publisher: { + select: { + id: true, + firstName: true, + lastName: true, + } + } + } + } + }, + }, }, }, }, }); - const assignments = publisher[0]?.assignments; + const assignments = publisher?.assignments || []; - const transformedAssignments = assignments.map(assignment => { + const transformedAssignments = assignments?.map(assignment => { if (assignment.shift && assignment.shift.startTime) { return { ...assignment, diff --git a/pages/cart/publishers/new.tsx b/pages/cart/publishers/new.tsx index fb99ae4..c0b1988 100644 --- a/pages/cart/publishers/new.tsx +++ b/pages/cart/publishers/new.tsx @@ -47,7 +47,7 @@ export const getServerSideProps = async (context) => { props: {} }; } - var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts"; + var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts"; console.log("GET PUBLISHER FROM:" + url) const { data } = await axios.get(url); diff --git a/pages/cart/publishers/stats.tsx b/pages/cart/publishers/stats.tsx index 6945583..d881b1f 100644 --- a/pages/cart/publishers/stats.tsx +++ b/pages/cart/publishers/stats.tsx @@ -41,7 +41,7 @@ function ContactsPage({ publishers }) { {filteredPublishers.map((pub) => ( - {pub.firstName} {pub.lastName} + {pub.firstName} {pub.lastName} {pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0} @@ -74,16 +74,23 @@ export default ContactsPage; export const getServerSideProps = async (context) => { const dateStr = new Date().toISOString().split('T')[0]; - let publishers = await filterPublishers('id,firstName,lastName,email,isactive,desiredShiftsPerMonth', "", new Date(), true, true, false); + let publishers = await filterPublishers('id,firstName,lastName,email,isActive,desiredShiftsPerMonth', "", new Date(), true, true, false); // const axios = await axiosServer(context); - // const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`); + // const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); - // api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2Cisactive%2CdesiredShiftsPerMonth + // api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2CisActive%2CdesiredShiftsPerMonth publishers.forEach(publisher => { publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0; publisher.assignments = publisher.assignments || []; publisher.availabilities = publisher.availabilities || []; + publisher.lastUpdate = publisher.availabilities.reduce((acc, curr) => curr.dateOfEntry > acc ? curr.dateOfEntry : acc, null); + if (publisher.lastUpdate) { + publisher.lastUpdate = common.getDateFormated(publisher.lastUpdate); + } + else { + publisher.lastUpdate = "Няма данни"; + } //serialize dates in publisher.assignments and publisher.availabilities publisher.assignments.forEach(assignment => { if (assignment.shift && assignment.shift.startTime) { @@ -95,10 +102,18 @@ export const getServerSideProps = async (context) => { if (availability.startTime) { availability.startTime = availability.startTime.toISOString(); availability.endTime = availability.endTime.toISOString(); + if (availability.dateOfEntry) { + availability.dateOfEntry = availability.dateOfEntry.toISOString(); + } } }); + //remove availabilities that isFromPreviousAssignment + publisher.availabilities = publisher.availabilities.filter(availability => !availability.isFromPreviousAssignment); + }); + //remove publishers without availabilities + publishers = publishers.filter(publisher => publisher.availabilities.length > 0); return { props: { diff --git a/pages/cart/reports/experience.tsx b/pages/cart/reports/experience.tsx index 5cb4db8..9e4df08 100644 --- a/pages/cart/reports/experience.tsx +++ b/pages/cart/reports/experience.tsx @@ -32,7 +32,7 @@ export const getServerSideProps = async (context) => { // }; // } // const { data: loc } = await axiosInstance.get( - // `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id + // `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id // ); // console.log(location) //this is the location object diff --git a/pages/cart/reports/list.tsx b/pages/cart/reports/list.tsx index d16e9a2..3675b55 100644 --- a/pages/cart/reports/list.tsx +++ b/pages/cart/reports/list.tsx @@ -22,7 +22,7 @@ export default function Reports() { const deleteReport = (id) => { axiosInstance - .delete(`api/data/reports/${id}`) + .delete(`/api/data/reports/${id}`) .then((res) => { toast.success("Успешно изтрит отчет"); // router.push("/cart/reports/list"); @@ -43,12 +43,12 @@ export default function Reports() { const { data } = await axiosInstance.get("/api/data/locations"); setLocations(data); console.log(data); - axiosInstance.get(`/api/data/reports`) + axiosInstance.get(`/api/data/reports?include=publisher,location`) .then((res) => { - let reports = res.data; - reports.forEach((report) => { - report.location = data.find((loc) => loc.id === report.locationId); - }); + // let reports = res.data; + // reports.forEach((report) => { + // report.location = data.find((loc) => loc.id === report.locationId); + // }); setReports(res.data); }) .catch((err) => { @@ -74,10 +74,23 @@ export default function Reports() { Добави нов отчет + + +
+ @@ -87,9 +100,10 @@ export default function Reports() { {reports.map((report) => ( - - - + + +
От Дата Място Отчет
{common.getDateFormated(new Date(report.date))}{report.location?.name} + {report.publisher.firstName + " " + report.publisher.lastName}{common.getDateFormated(new Date(report.date))}{report.location?.name} {(report.experienceInfo === null || report.experienceInfo === "") ? ( <> @@ -99,12 +113,19 @@ export default function Reports() { Клипове: {report.videoCount}
Адреси / Телефони: {report.returnVisitInfoCount}
+ ) : (report.placementCount > 0) ? ( + <> +
Отзив
+
+ ) : ( <>
Случка
- )} + ) + + }
@@ -122,8 +143,8 @@ export default function Reports() { - - + + ); } diff --git a/pages/cart/reports/report.tsx b/pages/cart/reports/report.tsx index ef3b971..8c7bcff 100644 --- a/pages/cart/reports/report.tsx +++ b/pages/cart/reports/report.tsx @@ -33,7 +33,7 @@ export const getServerSideProps = async (context) => { // } // const { data: loc } = await axiosInstance.get( - // `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id + // `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id // ); // console.log(location) //this is the location object // context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); diff --git a/pages/contactUs.tsx b/pages/contactUs.tsx index a7cd1a8..6fb5088 100644 --- a/pages/contactUs.tsx +++ b/pages/contactUs.tsx @@ -24,7 +24,7 @@ const ContactsPage = () => { */ } - {/* + {/* Телеграм Телеграм */} diff --git a/pages/dash.tsx b/pages/dash.tsx index c5f8ced..71718db 100644 --- a/pages/dash.tsx +++ b/pages/dash.tsx @@ -86,7 +86,7 @@ async function getAvailabilities(userId) { select: { id: true, name: true, - isactive: true, + isActive: true, isFromPreviousAssignment: true, dayofweek: true, dayOfMonth: true, @@ -121,7 +121,7 @@ async function getAvailabilities(userId) { shiftId Int publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisherId String - isactive Boolean @default(true) + isActive Boolean @default(true) isConfirmed Boolean @default(false) isWithTransport Boolean @default(false) Report Report[] @@ -133,7 +133,7 @@ async function getAvailabilities(userId) { }, select: { id: true, - isTentative: true, + isBySystem: true, isConfirmed: true, isWithTransport: true, shift: { diff --git a/pages/message.tsx b/pages/message.tsx new file mode 100644 index 0000000..a3502a3 --- /dev/null +++ b/pages/message.tsx @@ -0,0 +1,27 @@ +// pages/message.js + +import { useRouter } from 'next/router'; +import Layout from "../components/layout"; + +export default function MessagePage() { + const router = useRouter(); + const messageStyles = { + error: "text-red-500", + warning: "text-yellow-500", + info: "text-blue-500", + }; + const { message, type = messageStyles.info, caption } = router.query; + + return ( + +
+
+

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

+

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

+
+
+
+ ); +} diff --git a/pages/permits.tsx b/pages/permits.tsx index 19ae2b0..2ffced7 100644 --- a/pages/permits.tsx +++ b/pages/permits.tsx @@ -9,10 +9,19 @@ const PDFViewerPage = ({ pdfFiles }) => { return ( -

Разрешителни

+

Разрешителни

{/* Adjust the 100px based on your header/footer size */} - + {/*

+ {pdfFiles.map((file, index) => ( +

+ + Свали: {file.name} + +

+ ))} +

*/} {pdfFiles.map((file, index) => ( + // // {index > 0 &&
} {/* Vertical line separator */} // { // {file.name} // //
-
- < object data={file.url} type="application/pdf" style={{ width: '100%', height: '100%' }}> -

Вашият браузър не поддържа PDFs файлове. Моля свалете файла за да го разгледате: Свали {file.name}.

-

Your browser does not support PDFs. Please download the PDF to view it: {file.name}.

- -
- + <>

+ + Свали: {file.name} + +

+
+ < object data={file.url} type="application/pdf" style={{ width: '100%', height: '100%' }}> +

Вашият браузър не поддържа PDFs файлове. Моля свалете файла за да го разгледате: Свали {file.name}.

+

Your browser does not support PDFs. Please download the PDF to view it: {file.name}.

+ +
+ ))}
diff --git a/prisma/administrative_scripts/create_user.sql b/prisma/administrative_scripts/create_user.sql new file mode 100644 index 0000000..b521ce0 --- /dev/null +++ b/prisma/administrative_scripts/create_user.sql @@ -0,0 +1,2 @@ +CREATE USER 'cart'@'%' IDENTIFIED BY 'cartpw'; +GRANT ALL PRIVILEGES ON `cart\_dev`.* TO 'cart'@'%' WITH GRANT OPTION; \ No newline at end of file diff --git a/prisma/administrative_scripts/fix_availability_dayofweek.sql b/prisma/administrative_scripts/fix_availability_dayofweek.sql new file mode 100644 index 0000000..3d87f75 --- /dev/null +++ b/prisma/administrative_scripts/fix_availability_dayofweek.sql @@ -0,0 +1,28 @@ +-- find +SELECT * +FROM availability +WHERE dayofweek = "Thursday" +AND DAYOFWEEK(startTime) <> 5; + + +--fix + +UPDATE availability +SET dayofweek = CASE DAYOFWEEK(startTime) + WHEN 1 THEN 'Sunday' + WHEN 2 THEN 'Monday' + WHEN 3 THEN 'Tuesday' + WHEN 4 THEN 'Wednesday' + WHEN 5 THEN 'Thursday' + WHEN 6 THEN 'Friday' + WHEN 7 THEN 'Saturday' +END +WHERE dayofweek != CASE DAYOFWEEK(startTime) + WHEN 1 THEN 'Sunday' + WHEN 2 THEN 'Monday' + WHEN 3 THEN 'Tuesday' + WHEN 4 THEN 'Wednesday' + WHEN 5 THEN 'Thursday' + WHEN 6 THEN 'Friday' + WHEN 7 THEN 'Saturday' +END; \ No newline at end of file diff --git a/prisma/bl/subqueries.js b/prisma/bl/subqueries.js index ab557e0..44f977f 100644 --- a/prisma/bl/subqueries.js +++ b/prisma/bl/subqueries.js @@ -13,7 +13,7 @@ export const publisherSelectWithAvCount = { select: { availability: { where: { - isactive: true + isActive: true } } @@ -40,7 +40,7 @@ export const publisherSelect = { // endTime: true, // dayOfMonth: true, // dayofweek: true, -// isactive: true, +// isActive: true, // count: 'Availability_count' // } // } \ No newline at end of file diff --git a/prisma/migrations/20221130072538_updates/migration.sql b/prisma/migrations/20221130072538_updates/migration.sql index 239a224..978530e 100644 --- a/prisma/migrations/20221130072538_updates/migration.sql +++ b/prisma/migrations/20221130072538_updates/migration.sql @@ -5,7 +5,7 @@ CREATE TABLE `Publisher` ( `lastName` VARCHAR(191) NOT NULL, `email` VARCHAR(191) NOT NULL, `phone` VARCHAR(191) NULL, - `isactive` BOOLEAN NOT NULL DEFAULT true, + `isActive` BOOLEAN NOT NULL DEFAULT true, `age` INTEGER NULL, UNIQUE INDEX `Publisher_email_key`(`email`), @@ -41,7 +41,7 @@ CREATE TABLE `Shift` ( `name` VARCHAR(191) NOT NULL, `startTime` DATETIME(3) NOT NULL, `endTime` DATETIME(3) NOT NULL, - `isactive` BOOLEAN NOT NULL DEFAULT true, + `isActive` BOOLEAN NOT NULL DEFAULT true, `requiresTransport` BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY (`id`) @@ -52,7 +52,7 @@ CREATE TABLE `Location` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `name` VARCHAR(191) NOT NULL, `address` VARCHAR(191) NOT NULL, - `isactive` BOOLEAN NOT NULL DEFAULT true, + `isActive` BOOLEAN NOT NULL DEFAULT true, `dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL, PRIMARY KEY (`id`) diff --git a/prisma/migrations/20221201223336_/migration.sql b/prisma/migrations/20221201223336_/migration.sql index 90fe9cb..446e110 100644 --- a/prisma/migrations/20221201223336_/migration.sql +++ b/prisma/migrations/20221201223336_/migration.sql @@ -6,10 +6,10 @@ */ -- AlterTable -ALTER TABLE `Availability` ADD COLUMN `isactive` BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE `Availability` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true; -- AlterTable -ALTER TABLE `CartEvent` ADD COLUMN `isactive` BOOLEAN NOT NULL DEFAULT true, +ALTER TABLE `CartEvent` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true, ADD COLUMN `locationId` INTEGER NOT NULL, ADD COLUMN `shiftDuration` INTEGER NOT NULL; diff --git a/prisma/migrations/20221217221944_publisher_to_shift_renamed_assignment/migration.sql b/prisma/migrations/20221217221944_publisher_to_shift_renamed_assignment/migration.sql index a466b86..cd87cb8 100644 --- a/prisma/migrations/20221217221944_publisher_to_shift_renamed_assignment/migration.sql +++ b/prisma/migrations/20221217221944_publisher_to_shift_renamed_assignment/migration.sql @@ -18,7 +18,7 @@ CREATE TABLE `Assignment` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `shiftId` INTEGER NOT NULL, `publisherId` INTEGER NOT NULL, - `isactive` BOOLEAN NOT NULL DEFAULT true, + `isActive` BOOLEAN NOT NULL DEFAULT true, PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/migrations/20240131113517_assignment_isactive_renamed_istetative/migration.sql b/prisma/migrations/20240131113517_assignment_isactive_renamed_istetative/migration.sql index 0d0703e..55bc254 100644 --- a/prisma/migrations/20240131113517_assignment_isactive_renamed_istetative/migration.sql +++ b/prisma/migrations/20240131113517_assignment_isactive_renamed_istetative/migration.sql @@ -1,11 +1,11 @@ /* Warnings: - - You are about to drop the column `isactive` on the `assignment` table. All the data in the column will be lost. + - You are about to drop the column `isActive` on the `assignment` table. All the data in the column will be lost. */ -- AlterTable -ALTER TABLE `Assignment` DROP COLUMN `isactive`, +ALTER TABLE `Assignment` DROP COLUMN `isActive`, ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false; -- AlterTable diff --git a/prisma/migrations/20240325214807_misc_renames/migration.sql b/prisma/migrations/20240325214807_misc_renames/migration.sql new file mode 100644 index 0000000..890be7a --- /dev/null +++ b/prisma/migrations/20240325214807_misc_renames/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `isTentative` on the `Assignment` table. All the data in the column will be lost. + +*/ + +-- AlterTable +ALTER TABLE `Assignment` + ADD COLUMN `isBySystem` BOOLEAN NOT NULL DEFAULT FALSE; + +-- Depending on your DBMS, you might need to execute one statement at a time. +-- Especially, the UPDATE statement should be run separately. +UPDATE `Assignment` SET `isBySystem` = isTentative; + +-- Drop the isTentative column +ALTER TABLE `Assignment` DROP COLUMN `isTentative`; + +-- AlterTable +ALTER TABLE `Report` + ADD COLUMN `type` ENUM('ServiceReport', 'Experience', 'Feedback_Problem', 'Feedback_Suggestion', 'Feedback') NOT NULL DEFAULT 'ServiceReport'; diff --git a/prisma/migrations/20240328162213_add_availability_self_ref/migration.sql b/prisma/migrations/20240328162213_add_availability_self_ref/migration.sql new file mode 100644 index 0000000..bd11572 --- /dev/null +++ b/prisma/migrations/20240328162213_add_availability_self_ref/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE `Availability` ADD COLUMN `parentAvailabilityId` INTEGER NULL; + +-- AddForeignKey +ALTER TABLE `Availability` ADD CONSTRAINT `Availability_parentAvailabilityId_fkey` FOREIGN KEY (`parentAvailabilityId`) REFERENCES `Availability`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240401231200_add_publisher_email_sbscription_options/migration.sql b/prisma/migrations/20240401231200_add_publisher_email_sbscription_options/migration.sql new file mode 100644 index 0000000..e7f91bc --- /dev/null +++ b/prisma/migrations/20240401231200_add_publisher_email_sbscription_options/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE `Publisher` ADD COLUMN `isSubscribedToCoverMe` BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN `isSubscribedToReminders` BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2ea7b1f..aa7ecdd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,7 +10,6 @@ // //to generate schema // > npx prisma -// GPT // This is a Prisma database schema definition, which describes the structure and relationships between various entities in the database. Here's a brief overview of the different models: // Publisher: Represents a publisher, with attributes such as first name, last name, email, phone, age, and availability. A publisher can have many availabilities and assignments, and can also have multiple user accounts and sessions. @@ -21,9 +20,10 @@ // Location: Represents a location where a cart event can take place. A location can have a name, address, and multiple cart events. // Overall, this schema seems to represent a system for managing publishers and their assignments to cart events, including their availabilities and locations. +//$env:DATABASE="{connection string}"; npx prisma migrate deploy datasource db { provider = "mysql" - url = env("DATABASE_URL") + url = env("DATABASE") } generator client { @@ -81,13 +81,21 @@ enum PublisherType { SpecialPioneer_Missionary } +enum ReportType { + ServiceReport + Experience + Feedback_Problem + Feedback_Suggestion + Feedback +} + model Publisher { id String @id @default(cuid()) firstName String lastName String email String @unique phone String? - isactive Boolean @default(true) + isActive Boolean @default(true) isImported Boolean @default(false) isTrained Boolean @default(false) age Int? @@ -98,20 +106,21 @@ model Publisher { userId String? @unique user User? @relation(fields: [userId], references: [id]) - 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") - alwaysAsFamily Boolean? @default(false) //NEW v1.0.1 // New field to indicate if the publisher always wants to be assigned with the family - type PublisherType @default(Publisher) - town String? - comments String? - reports Report[] - Message Message[] + role UserRole @default(USER) + desiredShiftsPerMonth Int @default(4) + isMale Boolean @default(true) + isNameForeign Boolean @default(false) + isSubscribedToCoverMe Boolean @default(false) + isSubscribedToReminders Boolean @default(false) + familyHeadId String? // Optional familyHeadId for each family member + familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id]) + familyMembers Publisher[] @relation("FamilyMember") + alwaysAsFamily Boolean? @default(false) //NEW v1.0.1 // New field to indicate if the publisher always wants to be assigned with the family + type PublisherType @default(Publisher) + town String? + comments String? + reports Report[] + Message Message[] } model Availability { @@ -124,7 +133,7 @@ model Availability { weekOfMonth Int? startTime DateTime endTime DateTime - isactive Boolean @default(true) + isActive Boolean @default(true) type AvailabilityType @default(Weekly) isWithTransportIn Boolean @default(false) isWithTransportOut Boolean @default(false) @@ -133,7 +142,10 @@ model Availability { repeatWeekly Boolean? // New field to indicate weekly repetition // until now dayofweek was used for repetition when dayOfMonth is null repeatFrequency Int? // New field to indicate repetition frequency endDate DateTime? // New field for the end date of repetition - dateOfEntry DateTime? //NEW v1.0.1 trade storage for intuintivity + dateOfEntry DateTime? //NEW v1.0.1 + parentAvailabilityId Int? + parentAvailability Availability? @relation("ParentAvailability", fields: [parentAvailabilityId], references: [id]) + ChildAvailabilities Availability[] @relation("ParentAvailability") } model CartEvent { @@ -143,7 +155,7 @@ model CartEvent { shiftDuration Int shifts Shift[] dayofweek DayOfWeek - isactive Boolean @default(true) + isActive Boolean @default(true) location Location @relation(fields: [locationId], references: [id]) locationId Int eventType EventType @default(PW_Cart) @@ -160,7 +172,7 @@ model Shift { name String startTime DateTime endTime DateTime - isactive Boolean @default(true) + isActive Boolean @default(true) requiresTransport Boolean @default(false) notes String? //date DateTime @@ -177,7 +189,7 @@ model Assignment { shiftId Int publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisherId String - isTentative Boolean @default(false) // if no availability for it, when importing previous schedules + isBySystem Boolean @default(false) // if no availability for it, when importing previous schedules isConfirmed Boolean @default(false) isWithTransport Boolean @default(false) isMailSent Boolean @default(false) @@ -190,7 +202,7 @@ model Location { id Int @id @default(autoincrement()) name String address String - isactive Boolean @default(true) + isActive Boolean @default(true) content String? @db.LongText cartEvents CartEvent[] reports Report[] @@ -219,7 +231,8 @@ model Report { returnVisitInfoCount Int? conversationCount Int? - experienceInfo String? @db.LongText + experienceInfo String? @db.LongText + type ReportType @default(ServiceReport) @@map("Report") } diff --git a/prisma/seed.sql b/prisma/seed.sql index f64a4f5..6261a5c 100644 --- a/prisma/seed.sql +++ b/prisma/seed.sql @@ -30,7 +30,7 @@ INSERT INTO `id`, `name`, `address`, - `isactive` + `isActive` ) VALUES ( 1, @@ -57,7 +57,7 @@ INSERT INTO `startTime`, `endTime`, `dayofweek`, - `isactive`, + `isActive`, `locationId`, `shiftDuration`, `eventType`, @@ -147,7 +147,7 @@ VALUES ( /*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */ ; --- INSERT INTO `cartevent` (`id`, `startTime`, `endTime`, `dayofweek`, `isactive`, `locationId`, `shiftDuration`, `eventType`, `numberOfPublishers`) +-- INSERT INTO `cartevent` (`id`, `startTime`, `endTime`, `dayofweek`, `isActive`, `locationId`, `shiftDuration`, `eventType`, `numberOfPublishers`) -- VALUES -- (2, '2023-12-27 07:00:33.174', '2023-12-27 16:00:33.174', 'Tuesday', 1, 2, 90, 'PW_Cart', 4), -- (3, '2023-12-28 07:00:33.174', '2023-12-28 16:00:33.174', 'Wednesday', 1, 3, 90, 'PW_Cart', 4), diff --git a/process.d.ts b/process.d.ts index b140ecb..b31f4de 100644 --- a/process.d.ts +++ b/process.d.ts @@ -1,6 +1,6 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXTAUTH_URL: string + PUBLIC_URL: string NEXTAUTH_SECRET: string GITHUB_ID: string GITHUB_SECRET: string diff --git a/public/content/permits/Разрешително за Март 24г.-промяна (1).pdf b/public/content/permits/Разрешително за Март 24г.-промяна (1).pdf new file mode 100644 index 0000000..9fdf45c Binary files /dev/null and b/public/content/permits/Разрешително за Март 24г.-промяна (1).pdf differ diff --git a/public/content/permits/Разрешително за Март 24г..pdf b/public/content/permits/Разрешително за Март 24г..pdf new file mode 100644 index 0000000..281f391 Binary files /dev/null and b/public/content/permits/Разрешително за Март 24г..pdf differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..399ffb7 Binary files /dev/null and b/public/favicon.png differ diff --git a/server.js b/server.js index bfd1b44..a2d5ad9 100644 --- a/server.js +++ b/server.js @@ -20,31 +20,26 @@ process.env.TZ = 'Europe/Sofia'; // Global variable to store the base URL let baseUrlGlobal; -// if (process.env.NODE_ENV === 'test') { -// // Load environment variables from .env.test -// require('dotenv').config({ path: '.env.test' }); -// } else { -// // Load default environment variables -// require('dotenv').config(); -// } - -console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); -require('dotenv').config({ - path: `.env.${process.env.NODE_ENV}` -}); - +console.log("initial process.env.APP_ENV = ", process.env.APP_ENV); +console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param +require('dotenv').config({ path: `.env.${process.env.APP_ENV}` }); console.log("process.env.NODE_ENV = ", process.env.NODE_ENV); -const PORT = process.env.NEXT_PUBLIC_PORT || 3000; -const HOST = process.env.NEXT_PUBLIC_HOST; +const PROTOCOL = process.env.PROTOCOL; +const PORT = process.env.PORT || 3000; +const HOST = process.env.HOST; + const dev = process.env.NODE_ENV !== "production"; -const PROTOCOL = process.env.NEXT_PUBLIC_PROTOCOL; const nextApp = next({ dev }); const nextHandler = nextApp.getRequestHandler(); -console.log("process.env.SSL_ENABLED = ", process.env.SSL_ENABLED); +console.log("process.env.PROTOCOL = ", process.env.PROTOCOL); +process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_PUBLIC_URL; //NEXTAUTH_URL mandatory for next-auth +console.log("process.env.NEXT_PUBLIC_PUBLIC_URL = ", process.env.NEXT_PUBLIC_PUBLIC_URL); console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL); -console.log("process.env.NEXT_PUBLIC_PORT = ", process.env.NEXT_PUBLIC_PORT); +console.log("process.env.PORT = ", process.env.PORT); console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT); +console.log("process.env.DATABASE_URL = ", process.env.DATABASE_URL); +console.log("process.env.DATABASE = ", process.env.DATABASE); //require('module-alias/register'); @@ -62,12 +57,37 @@ const uploadTmp = multer({ storage: storageMem }); const prisma = common.getPrismaClient(); +const server = express(); +//check if ssl is enabled +if (process.env.PROTOCOL === 'https') { + console.log("SSL_ENABLED = true"); + // Redirect from http to https + // server.use((req, res, next) => { + // if (req.headers['x-forwarded-proto'] !== 'https') { + // return res.redirect(`https://${req.headers.host}${req.url}`); + // } + // next(); + // }); + if (process.env.SSL_KEY && process.env.SSL_CERT) { + const options = { + key: fs.readFileSync(process.env.SSL_KEY), + cert: fs.readFileSync(process.env.SSL_CERT), + secureProtocol: 'TLSv1_2_method', // Example: Force TLS 1.2 + }; + https.createServer(options, server).listen(PORT); + } +} +else { + server.listen(PORT, (err) => { + if (err) throw err; + console.log(`> Ready on ${PROTOCOL}://${HOST}:${PORT}`); + }); +} // handlers nextApp .prepare() .then(() => { - const server = express(); // Add the middleware to set 'x-forwarded-host' header server.use((req, res, next) => { @@ -84,6 +104,8 @@ nextApp next(); }); server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico")); + // server.use("/robots.txt", express.static("styles/favicon_io/robots.txt")); + // server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml")); server.get("/last_schedule_json", (req, res) => { // var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8")); @@ -259,7 +281,7 @@ nextApp var shifts = await prisma.shift.findMany({ where: { - isactive: true, + isActive: true, startTime: { gte: fromDate, lt: toDate, @@ -410,7 +432,7 @@ nextApp var publishers = await prisma.publisher.findMany({ where: { - isactive: true, + isActive: true, email: { not: "", }, @@ -556,31 +578,6 @@ nextApp return nextHandler(req, res); }); - //check if ssl is enabled - if (process.env.SSL_ENABLED === "true") { - console.log("SSL_ENABLED = true"); - // Redirect from http to https - // server.use((req, res, next) => { - // if (req.headers['x-forwarded-proto'] !== 'https') { - // return res.redirect(`https://${req.headers.host}${req.url}`); - // } - // next(); - // }); - if (process.env.SSL_KEY && process.env.SSL_CERT) { - const options = { - key: fs.readFileSync(process.env.SSL_KEY), - cert: fs.readFileSync(process.env.SSL_CERT), - secureProtocol: 'TLSv1_2_method', // Example: Force TLS 1.2 - }; - https.createServer(options, server).listen(PORT); - } - } - else { - server.listen(PORT, (err) => { - if (err) throw err; - console.log(`> Ready on ${PROTOCOL}://${HOST}:${PORT}`); - }); - } }) .catch((ex) => { console.warn(`Error starting server on ${HOST}:${PORT}`) diff --git a/src/helpers/calendar.js b/src/helpers/calendar.js index b98581b..4d4541a 100644 --- a/src/helpers/calendar.js +++ b/src/helpers/calendar.js @@ -474,7 +474,24 @@ createEvent = async (event) => { } }; +SaveEventsInGoogleCalendar = async function SaveEventsInGoogleCalendar(events) { + // Load client secrets from a local file. + try { + const content = await fs.readFile(CREDENTIALS_PATH); + // Authorize a client with credentials, then call the Google Calendar API. + authorize(JSON.parse(content), createEvent); + + + } catch (err) { + console.log("Error loading client secret file:", err); + } +}; + + + exports.GenerateICS = GenerateICS; exports.createEvent = createEvent; +exports.SaveEventsInGoogleCalendar = SaveEventsInGoogleCalendar; -createEvent(); + +//createEvent(); diff --git a/src/helpers/common.js b/src/helpers/common.js index c1508e1..a9a8b24 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -4,8 +4,9 @@ const levenshtein = require('fastest-levenshtein'); -const fs = require("fs"); -const path = require("path"); +const fs = typeof window === 'undefined' ? require('fs') : undefined; +const path = typeof window === 'undefined' ? require('path') : undefined; + const { PrismaClient } = require('@prisma/client'); const DayOfWeek = require("@prisma/client").DayOfWeek; @@ -75,53 +76,22 @@ exports.setBaseUrl = function (req) { }; exports.getBaseUrl = function (relative = "", req = null) { - const filePath = path.join(__dirname, 'baseUrl.txt'); - - try { - if (fs.existsSync(filePath)) { - const baseUrl = fs.readFileSync(filePath, 'utf8').trim(); - const fullUrl = relative ? new URL(relative, baseUrl).toString() : baseUrl; - return fullUrl; - } else { - if (req) { - const baseUrl = exports.setBaseUrl(req); // Correctly reference setBaseUrl - return `${baseUrl}/${relative.replace(/^\/|\/$/g, '')}`; - } - console.log('Base URL file does not exist.'); - return null; - } - } catch (error) { - console.error('Error reading the base URL file:', error); - return null; - } - // const host = process.env.NEXT_PUBLIC_HOST || '127.0.0.1'; - // const port = process.env.NEXT_PUBLIC_PORT ? `:${ process.env.NEXT_PUBLIC_PORT } ` : ''; - // const protocol = process.env.NEXT_PUBLIC_PROTOCOL || "https" - - // //const url = `${ protocol }://${host}${port}/${relative.replace(/^\/|\/$/g, '')}/`; - // const isRelativeEmpty = !relative || relative.trim() === ''; - // const formattedRelative = !isRelativeEmpty ? '/' + relative.replace(/^\/|\/$/g, '') : ''; - // const url = `${protocol}://${host}${port}${formattedRelative}`; - - - // logger.debug("NODE_ENV = ", process.env.NODE_ENV, "protocol:", protocol); - // logger.debug("getBaseURL = ", url); - - // return url; + return process.env.NEXT_PUBLIC_PUBLIC_URL + relative; }; - let prisma; exports.getPrismaClient = function getPrismaClient() { if (!prisma) { - logger.debug("getPrismaClient: process.env.DATABASE_URL = ", process.env.DATABASE_URL); + logger.debug("getPrismaClient: process.env.DATABASE = ", process.env.DATABASE); prisma = new PrismaClient({ // Optional: Enable logging //log: ['query', 'info', 'warn', 'error'], - datasources: { db: { url: process.env.DATABASE_URL } }, + datasources: { db: { url: process.env.DATABASE } }, }); } + logger.debug("getPrismaClient: process.env.DATABASE = ", process.env.DATABASE); + return prisma; } @@ -181,24 +151,27 @@ exports.getDayOfWeekName = function (date) { return exports.dayOfWeekNames[dayOfWeekIndex]; }; -exports.getDayOfWeekNameEnEnum = function (date) { +exports.getDayOfWeekNameEnEnumForDate = function (date) { date = new Date(date); const dayOfWeekIndex = date.getDayEuropean(); - const dayOfWeekNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; - - //return enum instead of string - // const dayOfWeekNames: Record = { - // 0: DayOfWeek.Monday, - // 1: DayOfWeek.Tuesday, - // 2: DayOfWeek.Wednesday, - // 3: DayOfWeek.Thursday, - // 4: DayOfWeek.Friday, - // 5: DayOfWeek.Saturday, - // 6: DayOfWeek.Sunday - // }; - - return dayOfWeekNames[dayOfWeekIndex]; + return exports.DaysOfWeekArray[dayOfWeekIndex]; } + +//obsolete: we want to ensure getDayEuropean() is used, hense we will not use this function +// exports.getDayOfWeekNameEnEnum = function (dayOfWeekIndex) { +// //return enum instead of string +// // const dayOfWeekNames: Record = { +// // 0: DayOfWeek.Monday, +// // 1: DayOfWeek.Tuesday, +// // 2: DayOfWeek.Wednesday, +// // 3: DayOfWeek.Thursday, +// // 4: DayOfWeek.Friday, +// // 5: DayOfWeek.Saturday, +// // 6: DayOfWeek.Sunday +// // }; +// return exports.DaysOfWeekArray[dayOfWeekIndex]; + +// } exports.getPubTypeEnum = function (text) { const input = text.trim(); const mapping = { @@ -228,15 +201,52 @@ exports.getDayOfWeekDate = function (dayOfWeekName, date = new Date()) { return date; }; //common.getWeekOfMonth(date) -// exports.getWeekOfMonth = function (date) { -// // Copy date so don't modify original -// date = new Date(date); -// // Adjust to Monday of this week -// date.setDate(date.getDate() + 3 - (date.getDayEuropean() + 6) % 7); -// // Return week number -// const weekNumber = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 / 7); -// return weekNumber; -// } +exports.getWeekOfMonth = function (inputDate) { + let date = new Date(inputDate); + let firstDayOfMonth = new Date(date.getFullYear(), date.getMonth(), 1); + let firstMonday = new Date(firstDayOfMonth); + + // Adjust firstDayOfMonth to the first Monday of the month + if (firstDayOfMonth.getDay() === 0) { // Sunday + firstMonday.setDate(2); + } else if (firstDayOfMonth.getDay() !== 1) { // Not Monday + firstMonday.setDate(9 - firstDayOfMonth.getDay()); + } + + // Calculate the difference in days + let diff = (date - firstMonday) / (1000 * 60 * 60 * 24); + // Calculate week number + let weekNumber = Math.ceil((diff + 1) / 7); + + return weekNumber; +}; + +exports.getDateFromWeekNrAndDayOfWeek = function (firstMonday, weekNr, dayOfWeekEnum, startTime) { + firstMonday = new Date(firstMonday); + startTime = new Date(startTime); + if (!weekNr || weekNr < 1 || weekNr > 5) { + weekNr = this.getWeekOfMonth(startTime); + } + //get int from dayOfWeekEnum + let dayOfWeekNr = this.getDayOfWeekIndex(dayOfWeekEnum); + if (dayOfWeekNr < 0 || dayOfWeekNr > 6) { + dayOfWeekNr = 0; + } + + // Calculate the day offset from the first Monday of the month + // Note: Assuming dayOfWeekEnum starts from 0 (Monday) to 6 (Sunday) + const daysFromFirstMonday = (weekNr - 1) * 7 + dayOfWeekNr; + + // Calculate the new date + let newStart = new Date(firstMonday); + newStart.setDate(firstMonday.getDate() + daysFromFirstMonday); + + // Extract time from startTime and apply it to newStart + const time = new Date(startTime); + newStart.setHours(time.getHours(), time.getMinutes(), time.getSeconds()); + + return newStart; +} exports.getMonthDatesInfo = function (date) { // get first day of the month @@ -275,6 +285,9 @@ exports.getMonthDatesInfo = function (date) { // lastSunday.setDate(firstDayNextMonth.getDate() - firstDayNextMonth.getDay()); //logger.debug("Last Sunday: ", lastSunday); + const diffInDays = (lastSunday - firstMonday) / (1000 * 60 * 60 * 24); + // Calculate number of weeks, rounding up for partial weeks + const nrOfWeeks = Math.ceil((diffInDays + 1) / 7); return { firstDay: firstDay, @@ -285,9 +298,10 @@ exports.getMonthDatesInfo = function (date) { date: date, monthName: monthName, year: date.getFullYear(), - nrOfWeeks: Math.ceil((lastMonday.getDate() - firstMonday.getDate()) / 7) + nrOfWeeks: nrOfWeeks }; }; +exports.getMonthInfo = exports.getMonthDatesInfo; exports.getMonthlyScheduleRange = function (date) { let info = exports.getMonthDatesInfo(date); @@ -511,7 +525,9 @@ exports.getCurrentYearMonth = () => { const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed 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 @@ -715,3 +731,7 @@ exports.getLocalStorage = function (key, defaultValue) { } return defaultValue; }; + +exports.root = function (req) { + return process.env.NEXT_PUBLIC_PUBLIC_URL; +} diff --git a/src/helpers/data.js b/src/helpers/data.js index 7c0916b..dc39ff2 100644 --- a/src/helpers/data.js +++ b/src/helpers/data.js @@ -79,10 +79,7 @@ async function findPublisher(names, email, select, getAll = false) { } async function findPublisherAvailability(publisherId, date) { - const prisma = common.getPrismaClient(); - const dayOfWeek = common.getDayOfWeekNameEnEnum(date); // Assuming common.getDayOfWeekNameEnEnum returns the day of week - //const weekOfMonth = common.getWeekOfMonth(date); // Assuming common.getWeekOfMonth returns the week of month date = new Date(date); // Convert to date object if not already const hours = date.getHours(); const minutes = date.getMinutes(); @@ -90,32 +87,24 @@ async function findPublisherAvailability(publisherId, date) { const potentialAvailabilities = await prisma.availability.findMany({ where: { publisherId: publisherId, - OR: [ + AND: [ // Ensure both conditions must be met { - // Exact date match startTime: { - gte: new Date(date.setHours(0, 0, 0, 0)), - lt: new Date(date.setHours(23, 59, 59, 999)) - } + lte: new Date(date), // startTime is less than or equal to the date + }, }, { - // Correct day of week and before the date, with endDate consideration - dayofweek: dayOfWeek, - OR: [ - { - endDate: null - }, - { - endDate: { - gt: date - } - } - ] - } - ] + endTime: { + gte: new Date(date), // endTime is greater than or equal to the date + }, + }, + ], } }); + if (potentialAvailabilities.length === 0) { + return null; // No availability found + } // Filter the results based on time and other criteria when not exact date match const availability = potentialAvailabilities.find(avail => { const availStartHours = avail.startTime.getHours(); @@ -147,7 +136,7 @@ async function getAvailabilities(userId) { select: { id: true, name: true, - isactive: true, + isActive: true, isFromPreviousAssignment: true, dayofweek: true, dayOfMonth: true, @@ -182,7 +171,7 @@ async function getAvailabilities(userId) { shiftId Int publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisherId String - isactive Boolean @default(true) + isActive Boolean @default(true) isConfirmed Boolean @default(false) isWithTransport Boolean @default(false) Report Report[] @@ -194,7 +183,7 @@ async function getAvailabilities(userId) { }, select: { id: true, - isTentative: true, + isBySystem: true, isConfirmed: true, isWithTransport: true, shift: { diff --git a/src/helpers/email.js b/src/helpers/email.js index f73ef5f..a69db91 100644 --- a/src/helpers/email.js +++ b/src/helpers/email.js @@ -1,10 +1,12 @@ // helper module to send emails with nodemailer const fs = require("fs"); +const path = require('path'); const { MailtrapClient } = require("mailtrap"); const nodemailer = require("nodemailer"); const CON = require("./const"); const CAL = require("./calendar"); +const Handlebars = require('handlebars'); // const { google } = require("googleapis"); // const OAuth2 = google.auth.OAuth2; @@ -12,14 +14,42 @@ const CAL = require("./calendar"); const { Shift, Publisher, PrismaClient } = require("@prisma/client"); const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad"; -const SENDER_EMAIL = "pw@d-popov.com"; -const sender = { name: "JW Cart: Shift Info", email: SENDER_EMAIL }; +const SENDER_EMAIL = "sofia@mwitnessing.com"; +const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL }; const client = new MailtrapClient({ token: TOKEN }); -const mailtrapTestClient = new MailtrapClient({ - username: '8ec69527ff2104',//not working now - password: 'c7bc05f171c96c' -}); +let mailtrapTestClient = null; +// const mailtrapTestClient = new MailtrapClient({ +// username: '8ec69527ff2104',//not working now +// password: 'c7bc05f171c96c' +// }); +//test +var transporter = nodemailer.createTransport({ + host: "sandbox.smtp.mailtrap.io", + port: 2525, + auth: { + user: "8ec69527ff2104", + pass: "c7bc05f171c96c" + } +}); +// production +// var transporter = nodemailer.createTransport({ +// host: "live.smtp.mailtrap.io", +// port: 587, +// auth: { +// user: "api", +// pass: "1cfe82e747b8dc3390ed08bb16e0f48d" +// } +// }); + +var transporterBulk = nodemailer.createTransport({ + host: "bulk.smtp.mailtrap.io", + port: 587, + auth: { + user: "api", + pass: "1cfe82e747b8dc3390ed08bb16e0f48d" + } +}); // ------------------ Email sending ------------------ var lastResult = null; function setResult(result) { @@ -29,44 +59,194 @@ exports.GetLastResult = function () { return lastResult; }; -exports.SendEmail = async function (to, subject, text, html) { - const message = { - from: sender, - to, - subject, - text, - html, - }; -}; +function normalizeEmailAddresses(to) { + let emails = []; -exports.SendEmail_Test = async function (to, subject, text, html) { - const message = { - from: sender, - to, - subject, - text, - html, - }; + if (typeof to === 'string') { + // Handle CSV string by splitting into an array + if (to.includes(',')) emails = to.split(/\s*,\s*/); + else emails = [to]; // Handle single email string + } else if (Array.isArray(to)) { + emails = to.map(item => { + if (typeof item === 'string') return item; + if (item.name && item.email) return `"${item.name}" <${item.email}>`; + return JSON.stringify(item); // Handle unexpected object format + }); + } else if (typeof to === 'object' && to.email) { + // Handle single object + emails = [`"${to.name}" <${to.email}>`]; + } else { + // Fallback for other types + emails = [String(to)]; + } - await mailtrapTestClient - .send(message) - .then(console.log, console.error, setResult); + return emails; // Always returns an array } +exports.SendEmail = async function (to, subject, text, html, attachments = []) { + let sender = '"Специално Свидетелстване София - тест" '; + const emailAddresses = normalizeEmailAddresses(to) + + const message = { + from: sender, + to: emailAddresses, + subject, + text, + html, + attachments + }; + + if (mailtrapTestClient !== null) { + // Assuming mailtrapTestClient is correctly set up to send emails + await mailtrapTestClient + .send(message) + .then(console.log) + .catch(console.error); + + } else { + + let result = await transporter + .sendMail(message) + .then(console.log) + .catch(console.error); + return result; + } + +}; + +exports.SendEmailHandlebars = async function (to, templateName, model, attachments = []) { + try { + // Ensure the sender and mailtrapTestClient are correctly defined or imported + + // Load and compile the main template + const mainTemplateSource = fs.readFileSync(path.join(process.cwd(), 'src', 'templates', 'emails', 'main.hbs'), 'utf8'); + const mainTemplate = Handlebars.compile(mainTemplateSource); + + // Dynamically load and compile the specified template + const templateSource = fs.readFileSync(path.join(process.cwd(), 'src', 'templates', 'emails', `${templateName}.hbs`), 'utf8'); + + // Extract subject and optional text version from the template source + const subjectMatch = templateSource.match(/{{!--\s*Subject:\s*(.*?)\s*--}}/); + const textMatch = templateSource.match(/{{!--\s*Text:\s*([\s\S]*?)\s*--}}/); + + let subject = subjectMatch ? subjectMatch[1].trim() : 'ССС: Известие'; + let textVersion = textMatch ? textMatch[1].trim() : null; + + // Remove the subject and text annotations from the template source + const cleanTemplateSource = templateSource.replace(/{{!-- Subject: .* --}}/, '').replace(/{{!-- Text: [\s\S]*? --}}/, ''); + // const cleanTemplateSource = templateSource + // .replace(/{{!--\s*Subject:.*?--}}\s*/, '') + // .replace(/{{!--\s*Text:.*?--}}\s*/, ''); + // Compile the cleaned template + const template = Handlebars.compile(cleanTemplateSource); + + // Render the specified template with the provided model + const templateHtml = template(model); + + // Render the main template, inserting the specific template HTML + const html = mainTemplate({ body: templateHtml }); + + // Generate a plain text version if not explicitly provided + let text = textVersion || html.replace(/<[^>]*>?/gm, ''); // Simple regex to strip HTML tags. Might need refinement. + subject = Handlebars.compile(subject)(model); + text = Handlebars.compile(text)(model); + + let results = this.SendEmail(to, subject, text, html, attachments); + return results; + + } catch (error) { + console.error(error); + return new Error('Error sending email'); + } +}; + + +exports.SendEmail_NewShifts = async function (publisher, shifts) { + if (shifts.length === 0) return; + + var date = new Date(shifts[0].startTime); + + // Generate ICS calendar links for all shifts + const icsLink = CAL.GenerateICS(shifts); + + // Prepare the shifts string + const shiftStr = shifts.map((s) => { + return `${CON.weekdaysBG[s.startTime.getDay()]} ${CON.GetDateFormat(s.startTime)} at ${s.cartEvent.location.name} from ${CON.GetTimeFormat(s.startTime)} to ${CON.GetTimeFormat(s.endTime)}`; + }).join("
"); + + // Define the model for the Handlebars template + const model = { + publisherFirstName: publisher.firstName, + publisherLastName: publisher.lastName, + month: CON.monthNamesBG[date.getMonth()], + shifts: shiftStr, + sentDate: new Date().toLocaleDateString() // Assuming you want to include the sent date in the email + }; + + await exports.SendEmailHandlebars(publisher.email, "newShifts", model, + [{ + filename: "calendar.ics", + content: icsLink, + contentType: 'text/calendar' // Ensure this is correctly set for the ICS file + }] + ).then(console.log).catch(console.error); +}; + + + + +//----------------------- OLD ----------------------------- + +// exports.SendEmail_NewShifts = async function (publisher, shifts) { +// if (shifts.length == 0) return; + +// var date = new Date(shifts[0].startTime); + +// //generate ICS calendar links for all shifts +// const icsLink = CAL.GenerateICS(shifts); + +// const shftStr = shifts +// .map((s) => { +// return ` ${CON.weekdaysBG[s.startTime.getDay()] +// } ${CON.GetDateFormat(s.startTime)} ${s.cartEvent.location.name +// } ${CON.GetTimeFormat(s.startTime)} - ${CON.GetTimeFormat( +// s.endTime +// )}`; +// }) +// .join("\n"); + +// await client.send({ +// from: sender, +// to: [ +// { +// email: "dobromir.popov@gmail.com",//publisher.email, +// name: publisher.firstName + " " + publisher.lastName, +// }, +// ], +// subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()], +// text: +// "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" + +// "Ти регистриран да получавате известия за нови смени на количка.\n" + +// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` + +// ` ${shftStr} \n\n\n` + +// "Поздрави,\n" + +// "Специално Свидетелстване София", +// attachments: [ +// { +// filename: "calendar.ics", +// content_id: "calendar.ics", +// disposition: "inline", +// content: icsLink, +// }, +// ], +// }) +// .then(console.log, console.error, setResult); +// }; + + // https://mailtrap.io/blog/sending-emails-with-nodemailer/ -exports.SendTestEmail = async function (to) { - // await client - // .send({ - // from: sender, - // to: [{ email: RECIPIENT_EMAIL }], - // subject: "Hello from Mailtrap!", - // text: "Welcome to Mailtrap Sending!",Shift Info" - // }) - // .then(console.log, console.error, setResult); - - // return lastResult; - +exports.SendEmail_Example = async function (to) { const welcomeImage = fs.readFileSync( path.join(CON.contentPath, "welcome.png") ); @@ -113,50 +293,3 @@ exports.SendTestEmail = async function (to) { }) .then(console.log, console.error, setResult); }; - - -exports.SendEmail_NewShifts = async function (publisher, shifts) { - if (shifts.length == 0) return; - - var date = new Date(shifts[0].startTime); - - //generate ICS calendar links for all shifts - const icsLink = CAL.GenerateICS(shifts); - - const shftStr = shifts - .map((s) => { - return ` ${CON.weekdaysBG[s.startTime.getDay()] - } ${CON.GetDateFormat(s.startTime)} ${s.cartEvent.location.name - } ${CON.GetTimeFormat(s.startTime)} - ${CON.GetTimeFormat( - s.endTime - )}`; - }) - .join("\n"); - - await client.send({ - from: sender, - to: [ - { - email: "dobromir.popov@gmail.com",//publisher.email, - name: publisher.firstName + " " + publisher.lastName, - }, - ], - subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()], - text: - "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" + - "Ти регистриран да получавате известия за нови смени на количка.\n" + - `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` + - ` ${shftStr} \n\n\n` + - "Поздрави,\n" + - "Специално Свидетелстване София", - attachments: [ - { - filename: "calendar.ics", - content_id: "calendar.ics", - disposition: "inline", - content: icsLink, - }, - ], - }) - .then(console.log, console.error, setResult); -}; diff --git a/src/helpers/excel.js b/src/helpers/excel.js index 49a62f3..4bba712 100644 --- a/src/helpers/excel.js +++ b/src/helpers/excel.js @@ -323,21 +323,21 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb } var shifts = await prisma.shift.findMany({ where: { - isactive: true, + isActive: true, startTime: { gte: monthDatesInfo.firstMonday, lt: monthDatesInfo.lastSunday, }, } }); - var locations = await prisma.location.findMany({ where: { isactive: true, } }); + var locations = await prisma.location.findMany({ where: { isActive: true, } }); - var cartEvents = await prisma.cartEvent.findMany({ where: { isactive: true, } }); + var cartEvents = await prisma.cartEvent.findMany({ where: { isActive: true, } }); var publishers = await prisma.publisher.findMany({ - where: { isactive: true, }, + where: { isActive: true, }, include: { - availabilities: { where: { isactive: true, }, }, + availabilities: { where: { isActive: true, }, }, assignments: { include: { shift: true, }, }, }, }); @@ -388,7 +388,7 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb } - var dayofWeek = common.getDayOfWeekNameEnEnum(date); + var dayofWeek = common.getDayOfWeekNameEnEnumForDate(date); const cartEvent = cartEvents.find( (ce) => ce.locationId === location.id && @@ -404,20 +404,21 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb s.cartEventId === cartEvent.id && new Date(s.startTime).getTime() === new Date(start).getTime() ); - + // get only hh:mm from the date + let isTransportRequired = event.shiftNr == 1 || end.toLocaleTimeString().substring(0, 5) == cartEvent.endTime.toLocaleTimeString().substring(0, 5); if (!shift) { //if shiftnr = 1, notes = "Докарва" + event.transport //if shiftnr = 8, notes = "Взема" + event.transport - - let note = event.shiftNr === 1 ? "Докарва количка от Люлин - " + event.transport : - event.shiftNr === 6 ? "Прибира количка в Люлин - " + event.transport : ""; + let note = isTransportRequired ? event.transport : ""; + // "Докарва количка от Люлин/Прибира количка в Люлин" const shiftEntity = await prisma.shift.create({ data: { name: event.dayOfWeek + " " + event.dayOfMonth + ", " + start.toLocaleTimeString() + " - " + end.toLocaleTimeString(), startTime: start, endTime: end, notes: note, + requiresTransport: isTransportRequired, cartEvent: { connect: { id: cartEvent.id, @@ -471,7 +472,7 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb email: name.toLowerCase().replace(/ /g, "."), // + "@gmail.com" firstName: firstname, lastName: lastname, - isactive: true, + isActive: true, isImported: true, // role: "EXTERNAL", }; @@ -481,22 +482,22 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb // create availability with the same date as the event. //ToDo: add parameter to control if we want to create availability for each event. can be done whe we import previous shifts. // if (createAvailabilities) { - // const dayofWeek = common.getDayOfWeekNameEnEnum(date); + // const dayofWeek = common.getDayOfWeekNameEnEnumForDate(date); // const availability = await prisma.availability.create({ // data: { // publisherId: publisher.id, - // //date: date, // dayofweek: dayofWeek, // startTime: startTime, // endTime: endTime, - // name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`, + // name: `от график, ${publisher.firstName} ${publisher.lastName}`, // isFromPreviousAssignment: true, - // isactive: true, + // isActive: true, // }, // }); // console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`); // } - // const personResponse = await axiosInstance.post("/publishers", manualPub); + + const personResponse = await axiosInstance.post("/publishers", manualPub); // let personId = personResponse.data.id; } catch (e) { @@ -506,6 +507,13 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb } if (location != null && publisher != null && shift != null) { + let isWithTransport = false; + if (isTransportRequired) { + const pubInitials = publisher.firstName[0] + publisher.lastName[0]; + // get cotent after last - or long dash-`-` and remove spaces, trim dots and make lowercase + let transportInitials = event.transport.split("-").pop().replace(/[\s.]/g, "").toUpperCase(); + isWithTransport = transportInitials.includes(pubInitials); + } const assignment = await prisma.assignment.create({ data: { //publisherId: publisher.id, @@ -520,13 +528,15 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb id: shift.id, }, }, + isWithTransport: isWithTransport, }, }); //ToDo: fix findPublisherAvailability and creation of availabilities // check if there is an availability for this publisher on this date, and if not, create one + //ToDo: check if that works // const availability = await data.findPublisherAvailability(publisher.id, start); // if (!availability && createAvailabilities) { - // const dayofWeek = common.getDayOfWeekNameEnEnum(date); + // const dayofWeek = common.getDayOfWeekNameEnEnumForDate(date); // const availability = await prisma.availability.create({ // data: { // publisherId: publisher.id, @@ -537,9 +547,11 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb // endTime: end, // name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`, // isFromPreviousAssignment: true, + // isWithTransportIn: isWithTransport && event.shiftNr == 1, + // isWithTransportOut: isWithTransport && event.shiftNr > 1, // }, // }); - // console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`); + // console.log(`Created SYSTEM availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`); // } console.log(`Created assignment with ID ${assignment.id} for date '${date.toDateString()}' and location '${event.placeOfEvent}'. publisher: ${publisher.firstName} ${publisher.lastName}}`); diff --git a/src/helpers/imports.js b/src/helpers/imports.js deleted file mode 100644 index a866ff6..0000000 --- a/src/helpers/imports.js +++ /dev/null @@ -1,4 +0,0 @@ -//??? can we consolidate all imports into one file? -import ProtectedRoute from '../../../components/protectedRoute'; -import axiosInstance from '../../../src/axiosSecure'; -import Layout from "../../../components/layout"; \ No newline at end of file diff --git a/src/sql/dev-test.sql b/src/sql/dev-test.sql index d7fe46b..09a3621 100644 --- a/src/sql/dev-test.sql +++ b/src/sql/dev-test.sql @@ -11,7 +11,7 @@ SELECT DISTINCT Publisher.* FROM Publisher INNER JOIN Availability ON Availability.publisherId = Publisher.id WHERE - Availability.isactive = true + Availability.isActive = true AND ( ( Availability.dayOfMonth IS NOT NULL AND Availability.startTime <= '2023-03-30 13:00:00' @@ -30,7 +30,7 @@ SELECT DISTINCT Publisher.* FROM Publisher INNER JOIN Availability ON Availability.publisherId = Publisher.id WHERE - Availability.isactive = true + Availability.isActive = true AND (Availability.dayOfMonth = 5) clfuyo33e005aknvchf1wm3bu All publishers: 121; (118) unique, diff --git a/src/templates/emails/coverMe.hbs b/src/templates/emails/coverMe.hbs new file mode 100644 index 0000000..2855a11 --- /dev/null +++ b/src/templates/emails/coverMe.hbs @@ -0,0 +1,26 @@ +{{!--Subject: ССС: Нужен е заместник --}} + +
+

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

+

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

+

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

+ {{!--

Shift Details:

--}} +

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

+

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

+

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

+ {{!--

Thank you very much for considering my request.

+

Best regards,
{{name}}

--}} +
+
+

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

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

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

+

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

+

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

+

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

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

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

+

Здравейте,

+

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

+ {{!--

Shift Details:

--}} +

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

+

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

+

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

+ {{!--

Thank you very much for considering my request.

+

Best regards,
{{name}}

--}} +
+
+

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

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

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

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

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

+

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

+

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

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