Merge branch 'production' of https://git.d-popov.com/popov/mwhitnessing into production
This commit is contained in:
29
.env
29
.env
@ -8,6 +8,7 @@ NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
|
|||||||
|
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
# mysql
|
# mysql
|
||||||
|
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||||
|
|
||||||
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
||||||
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||||
@ -18,16 +19,18 @@ AZURE_AD_CLIENT_ID=9e13bedd-1f9d-4c23-910e-a806aba308b6 # Application (client) I
|
|||||||
AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret value
|
AZURE_AD_CLIENT_SECRET=5ic8Q~GQmW-IUhuxzVGx3BE-i30GXDSpjfMHcb~z #client secret value
|
||||||
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
AZURE_AD_TENANT_ID=f69d1a93-bfba-498a-9b60-e87c1bc26276
|
||||||
|
|
||||||
|
# First APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
||||||
|
|
||||||
APPLE_ID=com.mwhitnessing.sofia
|
|
||||||
APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjY5NzM5NywiZXhwIjoxNzI4MjQ5Mzk4LCJzdWIiOiJYQzU3UDlTWERLLmNvbS5td2hpdG5lc3Npbmcuc29maWEifQ.QDX9eoRWAKMd10iRMW9Od88-0H_oZ_B6sPG61fw-zjHbNOvlHG3ddfxY1AqfdSMvLrXg1URKM1lnxOB-OCxg4A
|
|
||||||
# with team in the ID?
|
|
||||||
#APPLE_ID=XC57P9SXDK.com.mwhitnessing.sofia
|
|
||||||
#APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
|
||||||
# to generate
|
|
||||||
APPLE_TEAM_ID=XC57P9SXDK
|
APPLE_TEAM_ID=XC57P9SXDK
|
||||||
APPLE_KEY_ID=TB3V355G5Y
|
APPLE_KEY_ID=TB3V355G5Y
|
||||||
APPLE_PRIVATE_KEY=
|
|
||||||
|
APPLE_APP_ID=com.mwhitnessing.sofia
|
||||||
|
APPLE_SECRET=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJpYXQiOjE3MTMzMDQ1OTMsImV4cCI6MTcyODg1NjU5MywiYXVkIjoiaHR0cHM6Ly9hcHBsZWlkLmFwcGxlLmNvbSIsImlzcyI6IlhDNTdQOVNYREsiLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.iO2prjQ_4P7F17R7LTJfG9zHluj59uUtm8DA1LbK49jVBMeGHQP_Az7s_yU5D-GeMHSwU7VnVHcaVKiGWT_Yjg
|
||||||
|
|
||||||
|
# with team in the ID?
|
||||||
|
#APPLE_APP_ID=XC57P9SXDK.com.mwhitnessing.sofia
|
||||||
|
#APPLE_SECRET=eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiWEM1N1A5U1hESyIsImlhdCI6MTcxMjE3ODM0MiwiZXhwIjoxNzI3NzMwMzQzLCJzdWIiOiJjb20ubXdoaXRuZXNzaW5nLnNvZmlhIn0.XceA0qUQi0tXg0GM_LkJkpNU5AqXLiSB2JlEVbHCB_nINbQTWkjtoWxfqmvdOkIzwKtvdQ8FFb-crK9no9Bbbw
|
||||||
|
# to generate
|
||||||
|
|
||||||
|
|
||||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
||||||
@ -45,18 +48,18 @@ GITHUB_SECRET=
|
|||||||
TWITTER_ID=
|
TWITTER_ID=
|
||||||
TWITTER_SECRET=
|
TWITTER_SECRET=
|
||||||
|
|
||||||
|
# EMAIL_BYPASS_TO=mwitnessing@gmail.com
|
||||||
|
EMAIL_SENDER='"ССОМ" <mwitnessing@gmail.com>'
|
||||||
# EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
# EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
||||||
EMAIL_FROM=noreply@mwitnessing.com
|
# ?EMAIL_FROM=noreply@mwitnessing.com
|
||||||
|
|
||||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
EMAIL_SERVICE=mailtrap
|
||||||
|
# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||||
MAILTRAP_HOST=sandbox.smtp.mailtrap.io
|
MAILTRAP_HOST=sandbox.smtp.mailtrap.io
|
||||||
|
MAILTRAP_PORT=2525
|
||||||
MAILTRAP_USER=8ec69527ff2104
|
MAILTRAP_USER=8ec69527ff2104
|
||||||
MAILTRAP_PASS=c7bc05f171c96c
|
MAILTRAP_PASS=c7bc05f171c96c
|
||||||
|
|
||||||
GMAIL_EMAIL_USERNAME=
|
|
||||||
GMAIL_EMAIL_APP_PASS=
|
|
||||||
|
|
||||||
TELEGRAM_BOT=false
|
TELEGRAM_BOT=false
|
||||||
TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM
|
TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM
|
||||||
|
|
||||||
|
@ -7,5 +7,12 @@ NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
|||||||
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_SENDER='"ССОМ [ТЕСТ] " <mwitnessing@gmail.com>'
|
||||||
|
# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||||
|
# MAILTRAP_HOST=sandbox.smtp.mailtrap.io
|
||||||
|
# MAILTRAP_USER=8ec69527ff2104
|
||||||
|
# MAILTRAP_PASS=c7bc05f171c96c
|
||||||
|
|
||||||
SSL_KEY=./certificates/localhost-key.pem
|
SSL_KEY=./certificates/localhost-key.pem
|
||||||
SSL_CERT=./certificates/localhost.pem
|
SSL_CERT=./certificates/localhost.pem
|
||||||
|
@ -7,9 +7,15 @@ NEXT_PUBLIC_PUBLIC_URL= https://sofia.mwitnessing.com
|
|||||||
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||||
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
||||||
|
# DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||||
|
|
||||||
|
|
||||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
EMAIL_BYPASS_TO=
|
||||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
EMAIL_SENDER='"Специално Свидетелстване София" <mwitnessing@gmail.com>'
|
||||||
MAILTRAP_USER=api
|
EMAIL_SERVICE=gmail
|
||||||
MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
EMAIL_GMAIL_USERNAME=mwitnessing
|
||||||
|
EMAIL_GMAIL_APP_PASS="acys uzsp eere qzyh"
|
||||||
|
# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||||
|
# MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||||
|
# MAILTRAP_USER=api
|
||||||
|
# MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
14
.env.test
14
.env.test
@ -10,26 +10,14 @@ NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
|||||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||||
DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
|
DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
|
||||||
|
|
||||||
APPLE_ID=
|
|
||||||
APPLE_TEAM_ID=
|
|
||||||
APPLE_PRIVATE_KEY=
|
|
||||||
APPLE_KEY_ID=
|
|
||||||
|
|
||||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
||||||
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
||||||
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
|
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
|
||||||
|
|
||||||
FACEBOOK_ID=
|
|
||||||
FACEBOOK_SECRET=
|
|
||||||
|
|
||||||
GITHUB_ID=
|
|
||||||
GITHUB_SECRET=
|
|
||||||
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
||||||
# GOOGLE_SECRET=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
|
# GOOGLE_SECRET=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
|
||||||
|
|
||||||
TWITTER_ID=
|
EMAIL_SERVICE=mailtrap
|
||||||
TWITTER_SECRET=
|
|
||||||
|
|
||||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||||
MAILTRAP_USER=api
|
MAILTRAP_USER=api
|
||||||
|
@ -19,7 +19,7 @@ services:
|
|||||||
- GIT_USERNAME=deploy
|
- GIT_USERNAME=deploy
|
||||||
- GIT_PASSWORD=L3Kr2R438u4F7
|
- GIT_PASSWORD=L3Kr2R438u4F7
|
||||||
command: sh -c " cd /app && npm install && npm run prod; tail -f /dev/null"
|
command: sh -c " cd /app && npm install && npm run prod; tail -f /dev/null"
|
||||||
#command: sh -c " cd /app && n
|
#command: sh -c " cd /app && tail -f /dev/null"
|
||||||
tty: true
|
tty: true
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -27,13 +27,15 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then
|
|||||||
rsync -av /tmp/clone/package.json /app/package.json || echo "Rsync failed: Issue copying package.json"
|
rsync -av /tmp/clone/package.json /app/package.json || echo "Rsync failed: Issue copying package.json"
|
||||||
rsync -av /tmp/clone/package-lock.json /app/package-lock.json || echo "Rsync failed: Issue copying package-lock.json"
|
rsync -av /tmp/clone/package-lock.json /app/package-lock.json || echo "Rsync failed: Issue copying package-lock.json"
|
||||||
rm -rf /app/node_modules
|
rm -rf /app/node_modules
|
||||||
cd /app
|
|
||||||
npm install --no-audit --no-fund --no-optional --omit=optional
|
|
||||||
yes | npx prisma generate
|
yes | npx prisma generate
|
||||||
else
|
else
|
||||||
echo "Package files have not changed. Skipping package installation."
|
echo "Package files have not changed. Skipping package installation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd /app
|
||||||
|
npm install --no-audit --no-fund --no-optional --omit=optional
|
||||||
|
npx next build
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
rm -rf /tmp/clone
|
rm -rf /tmp/clone
|
||||||
echo "Update process completed."
|
echo "Update process completed."
|
||||||
|
@ -4,14 +4,14 @@ import { SignJWT } from "jose"
|
|||||||
import { createPrivateKey } from "crypto"
|
import { createPrivateKey } from "crypto"
|
||||||
|
|
||||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||||
console.log(`
|
console.log(`
|
||||||
Creates a JWT from the components found at Apple.
|
Creates a JWT from the components found at Apple.
|
||||||
By default, the JWT has a 6 months expiry date.
|
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
|
Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
|
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
|
||||||
APPLE_ID=com.mwhitnessing.sofia
|
APPLE_APP_ID=com.mwhitnessing.sofia
|
||||||
APPLE_TEAM_ID=XC57P9SXDK
|
APPLE_TEAM_ID=XC57P9SXDK
|
||||||
APPLE_KEY_ID=TB3V355G5Y
|
APPLE_KEY_ID=TB3V355G5Y
|
||||||
APPLE_KEY
|
APPLE_KEY
|
||||||
@ -37,45 +37,45 @@ eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuY
|
|||||||
--exp Future date in seconds when the JWT expires
|
--exp Future date in seconds when the JWT expires
|
||||||
`)
|
`)
|
||||||
} else {
|
} else {
|
||||||
const args = process.argv.slice(2).reduce((acc, arg, i) => {
|
const args = process.argv.slice(2).reduce((acc, arg, i) => {
|
||||||
if (arg.match(/^--\w/)) {
|
if (arg.match(/^--\w/)) {
|
||||||
const key = arg.replace(/^--/, "").toLowerCase()
|
const key = arg.replace(/^--/, "").toLowerCase()
|
||||||
acc[key] = process.argv[i + 3]
|
acc[key] = process.argv[i + 3]
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
team_id,
|
team_id,
|
||||||
iss = team_id,
|
iss = team_id,
|
||||||
|
|
||||||
private_key,
|
private_key,
|
||||||
|
|
||||||
client_id,
|
client_id,
|
||||||
sub = client_id,
|
sub = client_id,
|
||||||
|
|
||||||
key_id,
|
key_id,
|
||||||
kid = key_id,
|
kid = key_id,
|
||||||
|
|
||||||
expires_in = 86400 * 180,
|
expires_in = 86400 * 180,
|
||||||
exp = Math.ceil(Date.now() / 1000) + expires_in,
|
exp = Math.ceil(Date.now() / 1000) + expires_in,
|
||||||
} = args
|
} = args
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long is the secret valid in seconds.
|
* How long is the secret valid in seconds.
|
||||||
* @default 15780000
|
* @default 15780000
|
||||||
*/
|
*/
|
||||||
const expiresAt = Math.ceil(Date.now() / 1000) + expires_in
|
const expiresAt = Math.ceil(Date.now() / 1000) + expires_in
|
||||||
const expirationTime = exp ?? expiresAt
|
const expirationTime = exp ?? expiresAt
|
||||||
console.log(`
|
console.log(`
|
||||||
Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)}
|
Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)}
|
||||||
|
|
||||||
${await new SignJWT({})
|
${await new SignJWT({})
|
||||||
.setAudience("https://appleid.apple.com")
|
.setAudience("https://appleid.apple.com")
|
||||||
.setIssuer(iss)
|
.setIssuer(iss)
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setExpirationTime(expirationTime)
|
.setExpirationTime(expirationTime)
|
||||||
.setSubject(sub)
|
.setSubject(sub)
|
||||||
.setProtectedHeader({ alg: "ES256", kid })
|
.setProtectedHeader({ alg: "ES256", kid })
|
||||||
.sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
|
.sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
|
||||||
}
|
}
|
12
_doc/ToDo.md
12
_doc/ToDo.md
@ -207,6 +207,18 @@ push notifications
|
|||||||
store replacement
|
store replacement
|
||||||
test email
|
test email
|
||||||
|
|
||||||
|
problem with my repeating availability3
|
||||||
|
relax add/remove transport for publishers
|
||||||
|
|
||||||
|
fix published schedule to cover end of the week
|
||||||
|
|
||||||
|
имейлите - ОК
|
||||||
|
графика - синк - ОК
|
||||||
|
вестителите от Фабио -
|
||||||
|
потребителите с двойни имейли -
|
||||||
|
|
||||||
|
|
||||||
|
админс can send *urgent* email to everybody to ask for shift
|
||||||
|
in schedule admin - if a publisher is always pair & family is not in the shift - add + button to add them
|
||||||
постоянен лог
|
постоянен лог
|
||||||
лог ако е изрит потребител.
|
лог ако е изрит потребител.
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
#!/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")))}`)
|
|
||||||
}
|
|
@ -151,7 +151,7 @@ function PwaManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetch('/api/notification', {
|
await fetch('/api/notify', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@ -219,6 +219,11 @@ function PwaManager() {
|
|||||||
<span className="align-middle">Телеграм</span>
|
<span className="align-middle">Телеграм</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4" target="_blank">
|
||||||
|
<span className="align-middle">Apple sign-in</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import { bgBG } from '../x-date-pickers/locales/bgBG';
|
|||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
//todo import Availability type from prisma schema
|
//todo import Availability type from prisma schema
|
||||||
|
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
@ -25,6 +27,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
indexUrl: "/cart/availabilities"
|
indexUrl: "/cart/availabilities"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const id = parseInt(router.query.id);
|
||||||
//coalsce existingItems to empty array
|
//coalsce existingItems to empty array
|
||||||
existingItems = existingItems || [];
|
existingItems = existingItems || [];
|
||||||
|
|
||||||
@ -74,7 +77,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
|
|
||||||
|
|
||||||
const fetchItemFromDB = async () => {
|
const fetchItemFromDB = async () => {
|
||||||
const id = parseInt(router.query.id);
|
|
||||||
if (existingItems.length == 0 && id) {
|
if (existingItems.length == 0 && id) {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
|
const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
|
||||||
@ -183,59 +185,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
return groupedIntervals;
|
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);
|
|
||||||
// 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
|
// Common function to set shared properties
|
||||||
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
||||||
let startTime = new Date(availability.startTime || day);
|
let startTime = new Date(availability.startTime || day);
|
||||||
@ -263,7 +212,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
availability.dayOfMonth = startTime.getDate();
|
availability.dayOfMonth = startTime.getDate();
|
||||||
availability.endDate = null;
|
availability.endDate = null;
|
||||||
}
|
}
|
||||||
|
availability.isFromPreviousMonth = false;
|
||||||
availability.dateOfEntry = new Date();
|
availability.dateOfEntry = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,22 +281,25 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
// console.log("AvailabilityForm: publisherId: " + publisher.id + ", id: " + availabilit .id, ", inline: " + isInline);
|
// console.log("AvailabilityForm: publisherId: " + publisher.id + ", id: " + availabilit .id, ", inline: " + isInline);
|
||||||
|
//ToDo: this is examplary function to be used in the future. replace all date/time related functions with this one
|
||||||
const generateTimeSlots = (start, end, increment, items) => {
|
const generateTimeSlots = (start, end, increment, items) => {
|
||||||
const slots = [];
|
const slots = [];
|
||||||
let currentTime = start.getTime();
|
let currentTime = start;
|
||||||
|
|
||||||
const endTime = end.getTime();
|
const baseDate = new Date(2000, 0, 1); // Use a constant date for all time comparisons
|
||||||
|
|
||||||
while (currentTime < endTime) {
|
while (isBefore(currentTime, end)) {
|
||||||
let slotStart = new Date(currentTime);
|
let slotStart = normalizeTime(currentTime, baseDate);
|
||||||
let slotEnd = new Date(currentTime + increment * 60000); // increment is in minutes
|
let slotEnd = normalizeTime(addMinutes(currentTime, increment), baseDate);
|
||||||
|
|
||||||
const isChecked = items.some(item =>
|
const isChecked = items.some(item => {
|
||||||
item.startTime && item.endTime &&
|
let itemStart = item.startTime ? normalizeTime(new Date(item.startTime), baseDate) : null;
|
||||||
(slotStart.getTime() < item.endTime.getTime()) &&
|
let itemEnd = item.endTime ? normalizeTime(new Date(item.endTime), baseDate) : null;
|
||||||
(slotEnd.getTime() > item.startTime.getTime())
|
|
||||||
);
|
return itemStart && itemEnd &&
|
||||||
|
(slotStart.getTime() < itemEnd.getTime()) &&
|
||||||
|
(slotEnd.getTime() > itemStart.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
slots.push({
|
slots.push({
|
||||||
startTime: slotStart,
|
startTime: slotStart,
|
||||||
@ -355,10 +307,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
isChecked: isChecked,
|
isChecked: isChecked,
|
||||||
});
|
});
|
||||||
|
|
||||||
currentTime += increment * 60000; // Increment in milliseconds (minutes to milliseconds)
|
currentTime = addMinutes(currentTime, increment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Add isFirst, isLast, and isWithTransport properties
|
|
||||||
if (slots.length > 0 && items?.length > 0) {
|
if (slots.length > 0 && items?.length > 0) {
|
||||||
slots[0].isFirst = true;
|
slots[0].isFirst = true;
|
||||||
slots[slots.length - 1].isLast = true;
|
slots[slots.length - 1].isLast = true;
|
||||||
@ -369,6 +320,16 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
return slots;
|
return slots;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Normalize the time part of a date by using a base date
|
||||||
|
function normalizeTime(date, baseDate) {
|
||||||
|
return set(baseDate, {
|
||||||
|
hours: getHours(date),
|
||||||
|
minutes: getMinutes(date),
|
||||||
|
seconds: getSeconds(date),
|
||||||
|
milliseconds: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
||||||
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
||||||
const handleAllDayChange = (e) => {
|
const handleAllDayChange = (e) => {
|
||||||
@ -467,13 +428,14 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
|||||||
<ToastContainer></ToastContainer>
|
<ToastContainer></ToastContainer>
|
||||||
<form id="formAv" className="form p-5 bg-white shadow-md rounded-lg" onSubmit={handleSubmit}>
|
<form id="formAv" className="form p-5 bg-white shadow-md rounded-lg" onSubmit={handleSubmit}>
|
||||||
<h3 className="text-xl font-semibold mb-5 text-gray-800 border-b pb-2">
|
<h3 className="text-xl font-semibold mb-5 text-gray-800 border-b pb-2">
|
||||||
{editMode ? "Редактирай" : "Нова"} възможност
|
{editMode ? "Редактирай" : "Нова"} възможност: {common.getDateFormatedShort(day)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns} localeText={bgBG} adapterLocale={bg}>
|
<LocalizationProvider dateAdapter={AdapterDateFns} localeText={bgBG} adapterLocale={bg}>
|
||||||
<div className="mb-2">
|
{/* <div className="mb-2">
|
||||||
<DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} />
|
<DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
|
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<label className="checkbox-container">
|
<label className="checkbox-container">
|
||||||
|
@ -10,7 +10,7 @@ const common = require('src/helpers/common');
|
|||||||
|
|
||||||
|
|
||||||
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
||||||
|
const [isDeleted, setIsDeleted] = useState(false);
|
||||||
const [assignments, setAssignments] = useState(shift.assignments);
|
const [assignments, setAssignments] = useState(shift.assignments);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [useFilterDate, setUseFilterDate] = useState(true);
|
const [useFilterDate, setUseFilterDate] = useState(true);
|
||||||
@ -24,24 +24,14 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
}, [shift.assignments]);
|
}, [shift.assignments]);
|
||||||
|
|
||||||
const handleShiftClick = (shiftId) => {
|
const handleShiftClick = (shiftId) => {
|
||||||
// console.log("onShiftSelect prop:", onShiftSelect);
|
|
||||||
// console.log("Shift clicked:", shift);
|
|
||||||
//shift.selectedPublisher = selectedPublisher;
|
|
||||||
if (onShiftSelect) {
|
if (onShiftSelect) {
|
||||||
onShiftSelect(shift);
|
onShiftSelect(shift);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePublisherClick = (publisher) => {
|
const handlePublisherClick = (publisher) => {
|
||||||
|
|
||||||
//toggle selected
|
|
||||||
// if (selectedPublisher != null) {
|
|
||||||
// setSelectedPublisher(null);
|
|
||||||
// }
|
|
||||||
// else {
|
|
||||||
setSelectedPublisher(publisher);
|
setSelectedPublisher(publisher);
|
||||||
|
|
||||||
|
|
||||||
console.log("Publisher clicked:", publisher, "selected publisher:", selectedPublisher);
|
console.log("Publisher clicked:", publisher, "selected publisher:", selectedPublisher);
|
||||||
shift.selectedPublisher = publisher;
|
shift.selectedPublisher = publisher;
|
||||||
if (onShiftSelect) {
|
if (onShiftSelect) {
|
||||||
@ -54,6 +44,17 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
common.copyToClipboard(null, publisher.firstName + ' ' + publisher.lastName);
|
common.copyToClipboard(null, publisher.firstName + ' ' + publisher.lastName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteShift = async (id) => {
|
||||||
|
try {
|
||||||
|
console.log("Removing shift with id:", id);
|
||||||
|
await axiosInstance.delete("/api/data/shifts/" + id);
|
||||||
|
setIsDeleted(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error removing shift:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const removeAssignment = async (id) => {
|
const removeAssignment = async (id) => {
|
||||||
try {
|
try {
|
||||||
console.log("Removing assignment with id:", id);
|
console.log("Removing assignment with id:", id);
|
||||||
@ -100,137 +101,162 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleRequiresTransport(shiftId): Promise<void> {
|
||||||
|
try {
|
||||||
|
shift.requiresTransport = !shift.requiresTransport;
|
||||||
|
const { data } = await axiosInstance.put("/api/data/shifts/" + shiftId,
|
||||||
|
{ requiresTransport: shift.requiresTransport })
|
||||||
|
.then(() => {
|
||||||
|
console.log("shift '" + shiftId + "' transport required:" + shift.requiresTransport);
|
||||||
|
// setTransportProvided(assignments.some(ass => ass.isWithTransport))
|
||||||
|
});
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
<>{!isDeleted && (
|
||||||
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
|
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
||||||
{/* Time Window Header */}
|
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
|
||||||
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
{/* Time Window Header */}
|
||||||
<span className="text-lg font-semibold">
|
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
||||||
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
<span className="flex text-lg font-semibold">
|
||||||
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
|
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
||||||
</span>
|
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
|
||||||
|
{/* Toggle for Transport Requirement */}
|
||||||
|
<label className="ml-4 flex items-center">
|
||||||
|
<input type="checkbox" checked={shift.requiresTransport}
|
||||||
|
onChange={() => toggleRequiresTransport(shift.id)}
|
||||||
|
className="form-checkbox h-5 w-5 text-green-600" />
|
||||||
|
<span className="ml-2 text-sm text-gray-700">транспорт</span>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
|
||||||
{/* Copy All Names Button */}
|
{/* Copy All Names Button */}
|
||||||
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
|
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
|
||||||
копирай имената {/* Placeholder for Copy icon */}
|
копирай имената {/* Placeholder for Copy icon */}
|
||||||
</button>
|
</button>
|
||||||
{/* Hint Message */}
|
{/* Hint Message */}
|
||||||
{showCopyHint && (
|
{showCopyHint && (
|
||||||
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
|
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
|
||||||
Имената са копирани
|
Имената са копирани
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Assignments */}
|
{/* Assignments */}
|
||||||
{assignments.map((ass, index) => {
|
{assignments.map((ass, index) => {
|
||||||
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
||||||
|
|
||||||
// Determine border styles
|
// Determine border styles
|
||||||
let borderStyles = '';
|
let borderStyles = '';
|
||||||
let canTransport = false;
|
let canTransport = false;
|
||||||
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
||||||
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers
|
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (publisherInfo.availabilityCount == 0) //user has never the form
|
|
||||||
{
|
|
||||||
borderStyles = 'border-2 border-orange-300 ';
|
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
//if there is no publisherInfo - draw red border - publisher is no longer available for the day!
|
if (publisherInfo.availabilityCount == 0) //user has never the form
|
||||||
if (!publisherInfo.availabilities || publisherInfo.availabilities.length == 0) {
|
{
|
||||||
borderStyles = 'border-2 border-red-500 ';
|
borderStyles = 'border-2 border-orange-300 ';
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
//if there is no publisherInfo - draw red border - publisher is no longer available for the day!
|
||||||
|
if (!publisherInfo.availabilities || publisherInfo.availabilities.length == 0) {
|
||||||
|
borderStyles = 'border-2 border-red-500 ';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
// checkig if the publisher is available for this assignment
|
||||||
|
const av = publisherInfo.availabilities?.find(av =>
|
||||||
|
av.startTime <= shift.startTime && av.endTime >= shift.endTime
|
||||||
|
);
|
||||||
|
if (av) {
|
||||||
|
borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions
|
||||||
|
ass.canTransport = av.isWithTransportIn || av.isWithTransportOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publisherInfo.hasUpToDateAvailabilities) {
|
||||||
|
//add green right border
|
||||||
|
borderStyles += 'border-r-2 border-green-300';
|
||||||
|
}
|
||||||
|
|
||||||
|
//the pub is the same time as last month
|
||||||
|
// if (publisherInfo.availabilities?.some(av =>
|
||||||
|
// (!av.dayOfMonth || av.isFromPreviousMonth) &&
|
||||||
|
// av.startTime <= ass.startTime &&
|
||||||
|
// av.endTime >= ass.endTime)) {
|
||||||
|
// borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions
|
||||||
|
// }
|
||||||
|
|
||||||
// checkig if the publisher is available for this assignment
|
|
||||||
const av = publisherInfo.availabilities?.find(av =>
|
|
||||||
av.startTime <= shift.startTime && av.endTime >= shift.endTime
|
|
||||||
);
|
|
||||||
if (av) {
|
|
||||||
borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions
|
|
||||||
ass.canTransport = av.isWithTransportIn || av.isWithTransportOut;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publisherInfo.hasUpToDateAvailabilities) {
|
}
|
||||||
//add green right border
|
|
||||||
borderStyles += 'border-r-2 border-green-300';
|
|
||||||
}
|
|
||||||
|
|
||||||
//the pub is the same time as last month
|
return (
|
||||||
// if (publisherInfo.availabilities?.some(av =>
|
<div key={index}
|
||||||
// (!av.dayOfMonth || av.isFromPreviousMonth) &&
|
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}
|
||||||
// av.startTime <= ass.startTime &&
|
>
|
||||||
// av.endTime >= ass.endTime)) {
|
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
|
||||||
// borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions
|
<span className="text-gray-700">{publisherInfo.firstName} {publisherInfo.lastName}</span>
|
||||||
// }
|
<div className="flex items-left" >
|
||||||
|
{/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */}
|
||||||
|
{shift.requiresTransport && (
|
||||||
|
<span
|
||||||
|
onClick={ass.canTransport || true ? () => toggleTransport(ass) : undefined}
|
||||||
|
className={`material-icons ${ass.isWithTransport ? 'text-green-500 font-bold' : (transportProvided ? 'text-gray-400 ' : 'text-orange-400 font-bold')} ${ass.canTransport || ass.isWithTransport || true ? ' cursor-pointer' : 'cursor-not-allowed'} px-3 py-1 ml-2 rounded-md`}
|
||||||
|
>
|
||||||
|
{ass.isWithTransport ? "транспорт" : ass.canTransport ? "може транспорт" : "без транспорт"} <LocalShippingIcon />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button onClick={() => removeAssignment(ass.id)} className="text-white bg-red-500 hover:bg-red-600 px-3 py-1 ml-2 rounded-md" >
|
||||||
|
махни
|
||||||
|
</button>
|
||||||
|
|
||||||
}
|
</div>
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index}
|
|
||||||
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
|
|
||||||
<span className="text-gray-700">{publisherInfo.firstName} {publisherInfo.lastName}</span>
|
|
||||||
<div className="flex items-left" >
|
|
||||||
{/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */}
|
|
||||||
{shift.requiresTransport && (
|
|
||||||
<span
|
|
||||||
onClick={ass.canTransport ? () => toggleTransport(ass) : undefined}
|
|
||||||
className={`material-icons ${ass.isWithTransport ? 'text-green-500 font-bold' : (transportProvided ? 'text-gray-400 ' : 'text-orange-400 font-bold')} ${ass.canTransport ? ' cursor-pointer' : 'cursor-not-allowed'} px-3 py-1 ml-2 rounded-md`}
|
|
||||||
>
|
|
||||||
{ass.isWithTransport ? "транспорт" : ass.canTransport ? "може транспорт" : "без транспорт"} <LocalShippingIcon />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button onClick={() => removeAssignment(ass.id)} className="text-white bg-red-500 hover:bg-red-600 px-3 py-1 ml-2 rounded-md" >
|
|
||||||
махни
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
|
|
||||||
|
|
||||||
{/* This is a placeholder for the dropdown to add a publisher. You'll need to implement or integrate a dropdown component */}
|
{/* This is a placeholder for the dropdown to add a publisher. You'll need to implement or integrate a dropdown component */}
|
||||||
|
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
{/* Add Button */}
|
{/* Add Button */}
|
||||||
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
|
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
|
||||||
добави {/* Placeholder for Add icon */}
|
добави участник{/* Placeholder for Add icon */}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{assignments.length == 0 && (
|
||||||
|
<button onClick={() => deleteShift(shift.id)} className="bg-red-500 text-white p-2 py-1 rounded-md"
|
||||||
|
>изтрий смяната</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modal for Publisher Search
|
{/* Modal for Publisher Search
|
||||||
forDate={new Date(shift.startTime)}
|
forDate={new Date(shift.startTime)}
|
||||||
*/}
|
*/}
|
||||||
<Modal isOpen={isModalOpen}
|
<Modal isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
forDate={new Date(shift.startTime)}
|
forDate={new Date(shift.startTime)}
|
||||||
useFilterDate={useFilterDate}
|
useFilterDate={useFilterDate}
|
||||||
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
||||||
|
|
||||||
<PublisherSearchBox
|
<PublisherSearchBox
|
||||||
selectedId={null}
|
selectedId={null}
|
||||||
isFocused={isModalOpen}
|
isFocused={isModalOpen}
|
||||||
filterDate={useFilterDate ? new Date(shift.startTime) : null}
|
filterDate={useFilterDate ? new Date(shift.startTime) : null}
|
||||||
onChange={(publisher) => {
|
onChange={(publisher) => {
|
||||||
// Add publisher as assignment logic
|
// Add publisher as assignment logic
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
addAssignment(publisher, shift.id);
|
addAssignment(publisher, shift.id);
|
||||||
}}
|
}}
|
||||||
showAllAuto={true}
|
showAllAuto={true}
|
||||||
showSearch={true}
|
showSearch={true}
|
||||||
showList={false}
|
showList={false}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div >
|
</div >
|
||||||
|
)}</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,13 @@ import { MdToday } from 'react-icons/md';
|
|||||||
|
|
||||||
import { useSwipeable } from 'react-swipeable';
|
import { useSwipeable } from 'react-swipeable';
|
||||||
import axiosInstance from '../../src/axiosSecure';
|
import axiosInstance from '../../src/axiosSecure';
|
||||||
import { set } from 'date-fns';
|
// import { set, format, addDays } from 'date-fns';
|
||||||
|
// import { isEqual, isSameDay, getHours, getMinutes } from 'date-fns';
|
||||||
|
import { filter } from 'jszip';
|
||||||
|
import e from 'express';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Set moment to use the Bulgarian locale
|
// Set moment to use the Bulgarian locale
|
||||||
moment.locale('bg');
|
moment.locale('bg');
|
||||||
@ -40,11 +46,13 @@ const messages = {
|
|||||||
const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||||
|
|
||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
const [currentView, setCurrentView] = useState('month');
|
//ToDo: see if we can optimize this
|
||||||
const [evts, setEvents] = useState(events); // Existing events
|
const [evts, setEvents] = useState(events); // Existing events
|
||||||
const [displayedEvents, setDisplayedEvents] = useState(evts); // Events to display in the calendar
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [selectedEvents, setSelectedEvents] = useState([]);
|
const [selectedEvents, setSelectedEvents] = useState([]);
|
||||||
|
const [displayedEvents, setDisplayedEvents] = useState(evts); // Events to display in the calendar
|
||||||
|
|
||||||
|
const [currentView, setCurrentView] = useState('month');
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [visibleRange, setVisibleRange] = useState(() => {
|
const [visibleRange, setVisibleRange] = useState(() => {
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setDate(1); // Set to the first day of the current month
|
start.setDate(1); // Set to the first day of the current month
|
||||||
@ -162,6 +170,31 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
occurrences.push(occurrence);
|
occurrences.push(occurrence);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const filterEvents = (evts, publisherId, startdate) => {
|
||||||
|
setDate(startdate); // Assuming setDate is a function that sets some state or context
|
||||||
|
|
||||||
|
const filterDayOfWeek = startdate.getDay(); // Sunday - 0, Monday - 1, ..., Saturday - 6
|
||||||
|
|
||||||
|
// Filter events based on the publisher ID and the start date/time
|
||||||
|
const existingEvents = evts?.filter(event => {
|
||||||
|
// Ensure the event belongs to the specified publisher
|
||||||
|
const isPublisherMatch = (event.publisher?.id || event.publisherId) === publisherId;
|
||||||
|
const eventDayOfWeek = event.startTime.getDay();
|
||||||
|
|
||||||
|
let isDateMatch;
|
||||||
|
const eventDate = new Date(event.startTime);
|
||||||
|
if (event.repeatWeekly && filterDayOfWeek === eventDayOfWeek) {
|
||||||
|
isDateMatch = true;
|
||||||
|
} else if (event.date) {
|
||||||
|
// Compare the full date. issameday is not working. do it manually
|
||||||
|
isDateMatch = eventDate.setHours(0, 0, 0, 0) === new Date(startdate).setHours(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPublisherMatch && isDateMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingEvents;
|
||||||
|
};
|
||||||
|
|
||||||
// Define min and max times
|
// Define min and max times
|
||||||
const minHour = 8; // 8:00 AM
|
const minHour = 8; // 8:00 AM
|
||||||
@ -172,12 +205,13 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
maxTime.setHours(maxHour, 0, 0);
|
maxTime.setHours(maxHour, 0, 0);
|
||||||
const totalHours = maxHour - minHour;
|
const totalHours = maxHour - minHour;
|
||||||
|
|
||||||
const handleSelect = ({ start, end }) => {
|
const handleSelect = ({ mode, start, end }) => {
|
||||||
const startdate = typeof start === 'string' ? new Date(start) : start;
|
const startdate = typeof start === 'string' ? new Date(start) : start;
|
||||||
const enddate = typeof end === 'string' ? new Date(end) : end;
|
const enddate = typeof end === 'string' ? new Date(end) : end;
|
||||||
|
|
||||||
if (!start || !end) return;
|
if (!start || !end) return;
|
||||||
if (startdate < new Date() || end < new Date() || startdate > end) return;
|
//readonly for past dates (ToDo: if not admin)
|
||||||
|
//if (startdate < new Date() || end < new Date() || startdate > end) return;
|
||||||
|
|
||||||
// Check if start and end are on the same day
|
// Check if start and end are on the same day
|
||||||
if (startdate.toDateString() !== enddate.toDateString()) {
|
if (startdate.toDateString() !== enddate.toDateString()) {
|
||||||
@ -198,52 +232,25 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
setDate(start);
|
setDate(start);
|
||||||
|
|
||||||
// get exising events for the selected date
|
// get exising events for the selected date
|
||||||
const existingEvents = evts?.filter(event => (event.publisher?.id || event.publisherId) === publisherId && new Date(event.date).toDateString() === startdate.toDateString());
|
//ToDo: properly fix this. filterEvents does not return the expcted results
|
||||||
// const existingEvents = evts?.filter(event => {
|
let existingEvents = filterEvents(evts, publisherId, startdate);
|
||||||
// return event.publisherId === publisherId &&
|
// if existingEvents is empty - create new with the selected range
|
||||||
// new Date(event.startTime).getFullYear() === start.getFullYear() &&
|
// if (existingEvents.length === 0) {
|
||||||
// new Date(event.startTime).getMonth() === start.getMonth() &&
|
// existingEvents = [{ startTime: start, endTime: end }];
|
||||||
// new Date(event.startTime).getDate() === start.getDate();
|
// }
|
||||||
// });
|
|
||||||
console.log("handleSelect: " + existingEvents);
|
console.log("handleSelect: " + existingEvents);
|
||||||
setSelectedEvents(existingEvents);
|
setSelectedEvents(existingEvents);
|
||||||
|
|
||||||
// setSelectedEvent({
|
|
||||||
// date: start,
|
|
||||||
// startTime: start,
|
|
||||||
// endTime: end,
|
|
||||||
// dayOfMonth: start.getDate(),
|
|
||||||
// isActive: true,
|
|
||||||
// publisherId: publisherId,
|
|
||||||
// // Add any other initial values needed
|
|
||||||
// //set dayOfMonth to null, so that we repeat the availability every week
|
|
||||||
// dayOfMonth: null,
|
|
||||||
|
|
||||||
// });
|
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEventClick = (event) => {
|
const handleEventClick = (event) => {
|
||||||
if (event.type === "assignment") return;
|
if (event.type === "assignment") return;
|
||||||
handleSelect({ start: event.startTime, end: event.endTime });
|
//select the whole day
|
||||||
// Handle event click
|
let start = new Date(event.startTime);
|
||||||
// const eventForEditing = {
|
start.setHours(0, 0, 0, 0);
|
||||||
// ...event,
|
let end = new Date(event.startTime);
|
||||||
// startTime: new Date(event.startTime),
|
end.setHours(23, 59, 59, 999);
|
||||||
// endTime: new Date(event.endTime),
|
handleSelect({ mode: 'select', start: start, end: end });
|
||||||
// publisherId: event.publisherId || event.publisher?.connect?.id,
|
|
||||||
// repeatWeekly: event.repeatWeekly || false,
|
|
||||||
// };
|
|
||||||
// //strip title, start, end and allDay properties
|
|
||||||
// delete eventForEditing.title;
|
|
||||||
// delete eventForEditing.start;
|
|
||||||
// delete eventForEditing.end;
|
|
||||||
// delete eventForEditing.type;
|
|
||||||
// delete eventForEditing.publisher
|
|
||||||
// console.log("handleEventClick: " + eventForEditing);
|
|
||||||
// setSelectedEvents([eventForEditing]);
|
|
||||||
// setIsModalOpen(true);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDialogClose = async (dialogEvent) => {
|
const handleDialogClose = async (dialogEvent) => {
|
||||||
@ -256,10 +263,8 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
newEvents.forEach(event => {
|
newEvents.forEach(event => {
|
||||||
event.startTime = new Date(event.startTime);
|
event.startTime = new Date(event.startTime);
|
||||||
event.endTime = new Date(event.endTime);
|
event.endTime = new Date(event.endTime);
|
||||||
|
|
||||||
});
|
});
|
||||||
setEvents(newEvents);
|
setEvents(newEvents);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("handleSave: ", dialogEvent);
|
console.log("handleSave: ", dialogEvent);
|
||||||
@ -269,6 +274,56 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const eventStyleGetter = (event, start, end, isSelected) => {
|
||||||
|
|
||||||
|
//console.log("eventStyleGetter: " + event);
|
||||||
|
let backgroundColor = '#3174ad'; // default color for calendar events - #3174ad
|
||||||
|
if (currentView === 'agenda') {
|
||||||
|
return { style: {} }
|
||||||
|
}
|
||||||
|
if (event.type === "assignment") {
|
||||||
|
//event.title = event.publisher.name; //ToDo: add other publishers names
|
||||||
|
}
|
||||||
|
if (event.type === "availability") {
|
||||||
|
|
||||||
|
}
|
||||||
|
if (event.isFromPreviousAssignment) { //ToDo: does it work?
|
||||||
|
// orange-500 from Tailwind CSS
|
||||||
|
backgroundColor = '#f56565';
|
||||||
|
}
|
||||||
|
if (event.isFromPreviousMonth) {
|
||||||
|
//gray
|
||||||
|
backgroundColor = '#a0aec0';
|
||||||
|
}
|
||||||
|
if (event.isActive) {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'assignment':
|
||||||
|
backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS
|
||||||
|
break;
|
||||||
|
case 'recurring':
|
||||||
|
backgroundColor = '#63b3ed'; // blue-300 from Tailwind CSS
|
||||||
|
break;
|
||||||
|
default: // availability
|
||||||
|
//backgroundColor = '#a0aec0'; // gray-400 from Tailwind CSS
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backgroundColor = '#a0aec0'; // Default color for inactive events
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
style: {
|
||||||
|
backgroundColor,
|
||||||
|
opacity: 0.8,
|
||||||
|
color: 'white',
|
||||||
|
border: '0px',
|
||||||
|
display: 'block',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Components
|
||||||
const EventWrapper = ({ event, style }) => {
|
const EventWrapper = ({ event, style }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
let eventStyle = {
|
let eventStyle = {
|
||||||
@ -386,51 +441,6 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventStyleGetter = (event, start, end, isSelected) => {
|
|
||||||
|
|
||||||
//console.log("eventStyleGetter: " + event);
|
|
||||||
let backgroundColor = '#3174ad'; // default color for calendar events - #3174ad
|
|
||||||
if (currentView === 'agenda') {
|
|
||||||
return { style: {} }
|
|
||||||
}
|
|
||||||
if (event.type === "assignment") {
|
|
||||||
//event.title = event.publisher.name; //ToDo: add other publishers names
|
|
||||||
}
|
|
||||||
if (event.type === "availability") {
|
|
||||||
|
|
||||||
}
|
|
||||||
if (event.isFromPreviousAssignment) { //ToDo: does it work?
|
|
||||||
// orange-500 from Tailwind CSS
|
|
||||||
backgroundColor = '#f56565';
|
|
||||||
}
|
|
||||||
if (event.isActive) {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'assignment':
|
|
||||||
backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS
|
|
||||||
break;
|
|
||||||
case 'recurring':
|
|
||||||
backgroundColor = '#63b3ed'; // blue-300 from Tailwind CSS
|
|
||||||
break;
|
|
||||||
default: // availability
|
|
||||||
//backgroundColor = '#a0aec0'; // gray-400 from Tailwind CSS
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backgroundColor = '#a0aec0'; // Default color for inactive events
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
style: {
|
|
||||||
backgroundColor,
|
|
||||||
opacity: 0.8,
|
|
||||||
color: 'white',
|
|
||||||
border: '0px',
|
|
||||||
display: 'block',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Toolbar Component
|
|
||||||
|
|
||||||
const CustomToolbar = ({ onNavigate, label, onView, view }) => {
|
const CustomToolbar = ({ onNavigate, label, onView, view }) => {
|
||||||
return (
|
return (
|
||||||
@ -463,6 +473,17 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const CustomEventAgenda = ({
|
||||||
|
event
|
||||||
|
}) => (
|
||||||
|
<span>
|
||||||
|
<em style={{ color: 'black' }}>{event.title}</em>
|
||||||
|
<p>{event.desc}</p>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<> <div {...handlers} className="flex flex-col"
|
<> <div {...handlers} className="flex flex-col"
|
||||||
>
|
>
|
||||||
@ -488,10 +509,15 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
|||||||
components={{
|
components={{
|
||||||
event: EventWrapper,
|
event: EventWrapper,
|
||||||
toolbar: CustomToolbar,
|
toolbar: CustomToolbar,
|
||||||
|
view: CustomEventAgenda,
|
||||||
|
agenda: {
|
||||||
|
event: CustomEventAgenda
|
||||||
|
},
|
||||||
// ... other custom components
|
// ... other custom components
|
||||||
}}
|
}}
|
||||||
eventPropGetter={(eventStyleGetter)}
|
eventPropGetter={(eventStyleGetter)}
|
||||||
date={date}
|
date={date}
|
||||||
|
showAllEvents={true}
|
||||||
onNavigate={setDate}
|
onNavigate={setDate}
|
||||||
className="rounded-lg shadow-lg"
|
className="rounded-lg shadow-lg"
|
||||||
/>
|
/>
|
||||||
|
@ -9,56 +9,63 @@ import Body from 'next/document'
|
|||||||
|
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { set } from "date-fns"
|
||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({ children }) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
|
||||||
|
|
||||||
// auto resize for tablets: disabled.
|
// Assuming that during SSR, we don't want the sidebar to be open.
|
||||||
// useEffect(() => {
|
const [isSmallScreen, setIsSmallScreen] = useState(true);
|
||||||
// // Function to check and set the state based on window width
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
// const handleResize = () => {
|
|
||||||
// if (window.innerWidth < 768) { // Assuming 768px as the breakpoint for mobile devices
|
|
||||||
// setIsSidebarOpen(false);
|
|
||||||
// } else {
|
|
||||||
// setIsSidebarOpen(true);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// // Set initial state
|
|
||||||
// handleResize();
|
|
||||||
// // Add event listener
|
|
||||||
// window.addEventListener('resize', handleResize);
|
|
||||||
// // Cleanup
|
|
||||||
// return () => window.removeEventListener('resize', handleResize);
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This function determines if we're on a small screen
|
||||||
|
const checkIfSmallScreen = () => window.innerWidth < 768;
|
||||||
|
|
||||||
|
// Set the initial screen size and sidebar state
|
||||||
|
const initialSmallScreenCheck = checkIfSmallScreen();
|
||||||
|
setIsSmallScreen(initialSmallScreenCheck);
|
||||||
|
setIsSidebarOpen(!initialSmallScreenCheck);
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const smallScreenCheck = checkIfSmallScreen();
|
||||||
|
setIsSmallScreen(smallScreenCheck);
|
||||||
|
|
||||||
|
// On small screens, we want to ensure the sidebar is not open by default when resizing
|
||||||
|
if (smallScreenCheck) {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
// Cleanup event listener on component unmount
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle the sidebar only when the button is clicked
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setIsSidebarOpen(!isSidebarOpen);
|
setIsSidebarOpen(!isSidebarOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We use `isSmallScreen` to determine the margin-left on small screens
|
||||||
|
// and use `isSidebarOpen` to determine if we need to push the content on large screens.
|
||||||
|
const marginLeftClass = isSmallScreen ? 'ml-0' : isSidebarOpen ? 'ml-60' : 'ml-6';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
// <div className="h-screen w-screen" >
|
|
||||||
// <div className="flex flex-col">
|
|
||||||
// <div className="flex flex-row h-screen">
|
|
||||||
// <ToastContainer />
|
|
||||||
// <Sidebar isSidebarOpen={isSidebarOpen} toggleSidebar={toggleSidebar} />
|
|
||||||
// <main className={`pr-10 transition-all duration-300 ${isSidebarOpen ? 'ml-64 w-[calc(100%-16rem)] ' : 'ml-0 w-[calc(100%)] '}`}>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row h-[90vh] w-screen ">
|
<div className="flex flex-row min-h-screen w-screen">
|
||||||
<ToastContainer position="top-center" style={{ zIndex: 9999 }} />
|
<ToastContainer position="top-center" style={{ zIndex: 9999 }} />
|
||||||
<Sidebar isSidebarOpen={isSidebarOpen} toggleSidebar={toggleSidebar} />
|
<Sidebar isSidebarOpen={isSidebarOpen} toggleSidebar={toggleSidebar} />
|
||||||
<main className={`w-full pr-10 transition-all h-[90vh] duration-300 ${isSidebarOpen ? 'ml-60 ' : 'ml-6'}`}>
|
<main className={`flex-1 transition-all duration-300 ${marginLeftClass}`}>
|
||||||
{children}
|
<div className="p-4 mx-auto pr-8 pl-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="justify-end items-center text-center ">
|
|
||||||
<Footer />
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ import DayOfWeek from "../DayOfWeek";
|
|||||||
import TextEditor from "../TextEditor";
|
import TextEditor from "../TextEditor";
|
||||||
import FileUploadWithPreview from 'components/FileUploadWithPreview ';
|
import FileUploadWithPreview from 'components/FileUploadWithPreview ';
|
||||||
|
|
||||||
import ProtectedRoute, { serverSideAuth } from "../..//components/protectedRoute";
|
import ProtectedRoute, { serverSideAuth } from "../../components/protectedRoute";
|
||||||
import { UserRole } from "@prisma/client";
|
import { UserRole } from "@prisma/client";
|
||||||
|
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
|
@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import axiosInstance from '../../src/axiosSecure';
|
import axiosInstance from '../../src/axiosSecure';
|
||||||
|
import ProtectedRoute, { serverSideAuth } from "../../components/protectedRoute";
|
||||||
|
|
||||||
//add months to date. works with negative numbers and numbers > 12
|
//add months to date. works with negative numbers and numbers > 12
|
||||||
export function addMonths(numOfMonths, date) {
|
export function addMonths(numOfMonths, date) {
|
||||||
@ -53,6 +54,23 @@ export default function PublisherCard({ publisher }) {
|
|||||||
console.log(JSON.stringify(error));
|
console.log(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleLoginAs = async (userId) => {
|
||||||
|
const response = await fetch('/api/auth/login-as', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// Assuming you have some context or state management to update the session
|
||||||
|
updateSession(data.session);
|
||||||
|
} else {
|
||||||
|
alert("Failed to impersonate user.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return isCardVisible ? (
|
return isCardVisible ? (
|
||||||
// className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
|
// className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
|
||||||
@ -89,6 +107,10 @@ export default function PublisherCard({ publisher }) {
|
|||||||
<path fillRule="evenodd" d="M4.293 4.293A1 1 0 015.707 3.707L10 8l4.293-4.293a1 1 0 111.414 1.414L11.414 9l4.293 4.293a1 1 0 01-1.414 1.414L10 10.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 9 4.293 4.707a1 1 0 010-1.414z" clipRule="evenodd" /> */}
|
<path fillRule="evenodd" d="M4.293 4.293A1 1 0 015.707 3.707L10 8l4.293-4.293a1 1 0 111.414 1.414L11.414 9l4.293 4.293a1 1 0 01-1.414 1.414L10 10.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 9 4.293 4.707a1 1 0 010-1.414z" clipRule="evenodd" /> */}
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<ProtectedRoute>
|
||||||
|
|
||||||
|
<button onClick={() => handleLoginAs(publisher.id)}>Login as</button>
|
||||||
|
</ProtectedRoute>
|
||||||
</div>
|
</div>
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.cardFadeOut {
|
.cardFadeOut {
|
||||||
|
147
components/publisher/SearchReplacement.js
Normal file
147
components/publisher/SearchReplacement.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axiosInstance from '../../src/axiosSecure';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { set } from 'date-fns';
|
||||||
|
|
||||||
|
function SearchReplacement({ shiftId, assignmentId }) {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
// Dummy endpoint and shiftId, replace with actual
|
||||||
|
const response = await axiosInstance.get('/api/?action=getPossibleShiftPublisherEmails&shiftId=' + shiftId);
|
||||||
|
setUsers(response.data);
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendCoverMeRequestByEmail = (selectedGroups) => {
|
||||||
|
// You can map 'selectedGroups' to determine which API calls to make
|
||||||
|
console.log("Selected Groups:", selectedGroups);
|
||||||
|
axiosInstance.post('/api/email?action=sendCoverMeRequestByEmail', {
|
||||||
|
assignmentId: assignmentId,
|
||||||
|
toSubscribed: selectedGroups.includes('subscribedPublishers'),
|
||||||
|
toAvailable: selectedGroups.includes('availablePublishers'),
|
||||||
|
}).then(response => {
|
||||||
|
console.log("response", response);
|
||||||
|
setShowModal(false);
|
||||||
|
//toast success and confirm the change
|
||||||
|
toast.success("Заявката за заместник е изпратена!", {
|
||||||
|
onClose: () => {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.log("error", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="mr-2 mb-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
onClick={() => fetchUsers(shiftId)}
|
||||||
|
>
|
||||||
|
Търси заместник
|
||||||
|
</button>
|
||||||
|
{
|
||||||
|
showModal && (
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={showModal}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
onConfirm={sendCoverMeRequestByEmail}
|
||||||
|
subscribedPublishers={users.subscribedPublishers}
|
||||||
|
availablePublishers={users.availablePublishers}
|
||||||
|
/>
|
||||||
|
// <ConfirmationModal
|
||||||
|
// isOpen={showModal}
|
||||||
|
// users={users}
|
||||||
|
// onClose={() => setShowModal(false)}
|
||||||
|
// onConfirm={(selectedUsers) => {
|
||||||
|
// console.log(selectedUsers); // Here you would call the email API
|
||||||
|
// setShowModal(false);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmationModal({ isOpen, onClose, onConfirm, subscribedPublishers, availablePublishers }) {
|
||||||
|
const [selectedGroups, setSelectedGroups] = useState([]);
|
||||||
|
|
||||||
|
const handleToggleGroup = (groupName) => {
|
||||||
|
setSelectedGroups(prev => {
|
||||||
|
if (prev.includes(groupName)) {
|
||||||
|
return prev.filter(name => name !== groupName);
|
||||||
|
} else {
|
||||||
|
return [...prev, groupName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50" onClick={onClose}></div>
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-lg z-10">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Можете да изпратите заявка за заместник до следните групи:</h2>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block mb-2">
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2 leading-tight"
|
||||||
|
checked={selectedGroups.includes('subscribedPublishers')}
|
||||||
|
onChange={() => handleToggleGroup('subscribedPublishers')}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">Абонирани:</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
{subscribedPublishers.map(pub => (
|
||||||
|
<span key={pub.id} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">{pub.name}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block mb-2">
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2 leading-tight"
|
||||||
|
checked={selectedGroups.includes('availablePublishers')}
|
||||||
|
onChange={() => handleToggleGroup('availablePublishers')}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">На разположение:</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
{availablePublishers.map(pub => (
|
||||||
|
<span key={pub.id} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">{pub.name}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<button
|
||||||
|
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 mr-2"
|
||||||
|
onClick={() => onConfirm(selectedGroups)}
|
||||||
|
>
|
||||||
|
Потвърждавам
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Отказ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default SearchReplacement;
|
@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import DayOfWeek from "../DayOfWeek";
|
import DayOfWeek from "../DayOfWeek";
|
||||||
import { Location, UserRole } from "@prisma/client";
|
import { ReportType } from "@prisma/client";
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
|
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
@ -97,6 +97,7 @@ export default function ExperienceForm({ publisherId, assgnmentId, existingItem,
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
item.publisher = { connect: { id: pubId } };
|
item.publisher = { connect: { id: pubId } };
|
||||||
item.location = { connect: { id: parseInt(item.locationId) } };
|
item.location = { connect: { id: parseInt(item.locationId) } };
|
||||||
|
item.type = ReportType.Experience;
|
||||||
delete item.locationId;
|
delete item.locationId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import DayOfWeek from "../DayOfWeek";
|
import DayOfWeek from "../DayOfWeek";
|
||||||
import { Location, UserRole } from "@prisma/client";
|
import { ReportType } from "@prisma/client";
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
|
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
@ -55,7 +55,7 @@ export default function FeedbackForm({ publisherId, onDone }) {
|
|||||||
assignmentId: 0,
|
assignmentId: 0,
|
||||||
publisherId: publisherId,
|
publisherId: publisherId,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
placementCount: 0,
|
placementCount: 1,
|
||||||
videoCount: 0,
|
videoCount: 0,
|
||||||
returnVisitInfoCount: 0,
|
returnVisitInfoCount: 0,
|
||||||
conversationCount: 0
|
conversationCount: 0
|
||||||
@ -73,6 +73,8 @@ export default function FeedbackForm({ publisherId, onDone }) {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
item.publisher = { connect: { id: pubId } };
|
item.publisher = { connect: { id: pubId } };
|
||||||
|
//ToDo: create dedicated feedback type instead of using placementCount for subtype
|
||||||
|
item.type = item.placementCount === 1 ? ReportType.Feedback_Problem : (item.placementCount === 2 ? ReportType.Feedback_Suggestion : ReportType.Feedback);
|
||||||
delete item.assignmentId;
|
delete item.assignmentId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
|
import { ReportType } from "@prisma/client";
|
||||||
|
|
||||||
const common = require('src/helpers/common');
|
const common = require('src/helpers/common');
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ export default function ReportForm({ shiftId, existingItem, onDone }) {
|
|||||||
item.publisher = { connect: { id: publisherId } };
|
item.publisher = { connect: { id: publisherId } };
|
||||||
item.shift = { connect: { id: parseInt(item.shiftId) } };
|
item.shift = { connect: { id: parseInt(item.shiftId) } };
|
||||||
item.date = new Date(item.date);
|
item.date = new Date(item.date);
|
||||||
|
item.type = ReportType.Report;
|
||||||
delete item.publisherId;
|
delete item.publisherId;
|
||||||
delete item.shiftId;
|
delete item.shiftId;
|
||||||
item.placementCount = parseInt(item.placementCount);
|
item.placementCount = parseInt(item.placementCount);
|
||||||
|
@ -129,7 +129,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
|
|||||||
className="fixed top-1 left-4 z-40 m- text-xl bg-white border border-gray-200 p-2 rounded-full shadow-lg focus:outline-none"
|
className="fixed top-1 left-4 z-40 m- text-xl bg-white border border-gray-200 p-2 rounded-full shadow-lg focus:outline-none"
|
||||||
style={{ transform: isSidebarOpen ? `translateX(${sidebarWidth - 64}px)` : 'translateX(-20px)' }}>☰</button>
|
style={{ transform: isSidebarOpen ? `translateX(${sidebarWidth - 64}px)` : 'translateX(-20px)' }}>☰</button>
|
||||||
<aside id="sidenav" ref={sidebarRef}
|
<aside id="sidenav" ref={sidebarRef}
|
||||||
className="px-2 fixed top-0 left-0 z-30 h-screen overflow-y-auto bg-white border-r dark:bg-gray-900 dark:border-gray-700 transition-all duration-300 w-64"
|
className="px-2 fixed top-0 left-0 z-30 h-screen overflow-y-auto bg-white border-r dark:bg-gray-900 dark:border-gray-700 transition-all duration-300 sm:translate-x-0 w-64"
|
||||||
style={{ width: `${sidebarWidth}px`, transform: isSidebarOpen ? 'translateX(0)' : `translateX(-${sidebarWidth - 16}px)` }}>
|
style={{ width: `${sidebarWidth}px`, transform: isSidebarOpen ? 'translateX(0)' : `translateX(-${sidebarWidth - 16}px)` }}>
|
||||||
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white pt-2 pl-4 pb-4"
|
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white pt-2 pl-4 pb-4"
|
||||||
title={`v.${packageVersion} ${process.env.GIT_COMMIT_ID}`} >Специално Свидетелстване София</h2>
|
title={`v.${packageVersion} ${process.env.GIT_COMMIT_ID}`} >Специално Свидетелстване София</h2>
|
||||||
@ -163,7 +163,7 @@ function UserSection({ session }) {
|
|||||||
|
|
||||||
function SignInButton() {
|
function SignInButton() {
|
||||||
return (
|
return (
|
||||||
<div className="items-center py-2" onClick={() => signIn()}>
|
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
|
||||||
<button>Впишете се</button>
|
<button>Впишете се</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
BIN
mwitnessing_totp_setup.png
Normal file
BIN
mwitnessing_totp_setup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -5,7 +5,7 @@ const withPWA = require('next-pwa')({
|
|||||||
register: true, // ?
|
register: true, // ?
|
||||||
publicExcludes: ["!_error*.js"], //?
|
publicExcludes: ["!_error*.js"], //?
|
||||||
|
|
||||||
disable: process.env.NODE_ENV === 'development',
|
//disable: process.env.NODE_ENV === 'development',
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = withPWA({
|
module.exports = withPWA({
|
||||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -4802,6 +4802,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clean-stack": "^2.0.0",
|
"clean-stack": "^2.0.0",
|
||||||
"indent-string": "^4.0.0"
|
"indent-string": "^4.0.0"
|
||||||
@ -5771,6 +5772,7 @@
|
|||||||
"url": "https://github.com/sponsors/sibiraj-s"
|
"url": "https://github.com/sponsors/sibiraj-s"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -5784,6 +5786,7 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||||
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -8546,7 +8549,8 @@
|
|||||||
"node_modules/hosted-git-info": {
|
"node_modules/hosted-git-info": {
|
||||||
"version": "2.8.9",
|
"version": "2.8.9",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
|
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/hsl-to-hex": {
|
"node_modules/hsl-to-hex": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -8888,6 +8892,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -8915,6 +8920,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
|
||||||
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
|
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
|
||||||
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -10943,6 +10949,7 @@
|
|||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hosted-git-info": "^2.1.4",
|
"hosted-git-info": "^2.1.4",
|
||||||
"resolve": "^1.10.0",
|
"resolve": "^1.10.0",
|
||||||
@ -10954,6 +10961,7 @@
|
|||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
@ -13870,6 +13878,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aggregate-error": "^3.0.0"
|
"aggregate-error": "^3.0.0"
|
||||||
},
|
},
|
||||||
@ -15871,6 +15880,7 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||||
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"spdx-expression-parse": "^3.0.0",
|
"spdx-expression-parse": "^3.0.0",
|
||||||
"spdx-license-ids": "^3.0.0"
|
"spdx-license-ids": "^3.0.0"
|
||||||
@ -15879,12 +15889,14 @@
|
|||||||
"node_modules/spdx-exceptions": {
|
"node_modules/spdx-exceptions": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
|
||||||
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
|
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/spdx-expression-parse": {
|
"node_modules/spdx-expression-parse": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"spdx-exceptions": "^2.1.0",
|
"spdx-exceptions": "^2.1.0",
|
||||||
"spdx-license-ids": "^3.0.0"
|
"spdx-license-ids": "^3.0.0"
|
||||||
@ -15893,7 +15905,8 @@
|
|||||||
"node_modules/spdx-license-ids": {
|
"node_modules/spdx-license-ids": {
|
||||||
"version": "3.0.17",
|
"version": "3.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
|
||||||
"integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg=="
|
"integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/ssf": {
|
"node_modules/ssf": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
@ -17442,6 +17455,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"spdx-correct": "^3.0.0",
|
"spdx-correct": "^3.0.0",
|
||||||
"spdx-expression-parse": "^3.0.0"
|
"spdx-expression-parse": "^3.0.0"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "JW PW Web App",
|
"description": "JW PW Web App",
|
||||||
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
||||||
@ -12,7 +12,7 @@
|
|||||||
"debug": "node server.js",
|
"debug": "node server.js",
|
||||||
"debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js",
|
"debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js",
|
||||||
"nodeenv": "dotenv -e .env.$APP_ENV -- node server.js",
|
"nodeenv": "dotenv -e .env.$APP_ENV -- node server.js",
|
||||||
"prod": "npx next build && dotenv -e .env.production -- node server.js",
|
"prod": "dotenv -e .env.production -- node server.js",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"buildWin": "npm run build",
|
"buildWin": "npm run build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
@ -113,4 +113,4 @@
|
|||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"prisma": "^5.12.1"
|
"prisma": "^5.12.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ const common = require("../../../src/helpers/common");
|
|||||||
import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt'
|
import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt'
|
||||||
|
|
||||||
|
|
||||||
|
console.log("appleID:", process.env.APPLE_APP_ID);
|
||||||
// console.log(process.env.EMAIL_SERVER)
|
// console.log(process.env.EMAIL_SERVER)
|
||||||
// For more information on each option (and a full list of options) go to
|
// For more information on each option (and a full list of options) go to
|
||||||
// https://next-auth.js.org/configuration/options
|
// https://next-auth.js.org/configuration/options
|
||||||
@ -41,10 +42,10 @@ export const authOptions: NextAuthOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
AppleProvider({
|
// AppleProvider({
|
||||||
clientId: process.env.APPLE_ID,
|
// clientId: process.env.APPLE_APP_ID,
|
||||||
clientSecret: process.env.APPLE_SECRET
|
// clientSecret: process.env.APPLE_SECRET
|
||||||
}),
|
// }),
|
||||||
// AzureADProvider({
|
// AzureADProvider({
|
||||||
// clientId: process.env.AZURE_AD_CLIENT_ID,
|
// clientId: process.env.AZURE_AD_CLIENT_ID,
|
||||||
// clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
// clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
||||||
|
45
pages/api/auth/apple-signin.ts
Normal file
45
pages/api/auth/apple-signin.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// pages/api/auth/apple.js
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import axios from 'axios';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const dotenv = require("dotenv");
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
// Generate the client secret
|
||||||
|
const clientSecret = generateClientSecret();
|
||||||
|
// const redirectUri = `${req.headers.origin}/api/auth/apple/callback`;
|
||||||
|
const redirectUri = `https://sofia.mwitnessing.com/api/auth/callback/apple`;
|
||||||
|
|
||||||
|
// Redirect to Apple's authorization page
|
||||||
|
const url = `https://appleid.apple.com/auth/authorize?response_type=code&client_id=${process.env.APPLE_APP_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=email&response_mode=form_post&state=initial&usePopup=true&client_secret=${encodeURIComponent(clientSecret)}`;
|
||||||
|
res.redirect(url);
|
||||||
|
} else {
|
||||||
|
res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateClientSecret() {
|
||||||
|
const appleKey = fs.readFileSync(path.resolve('./_deploy/appleKey.p8'), 'utf8');
|
||||||
|
const teamID = process.env.APPLE_TEAM_ID || "XC57P9SXDK";
|
||||||
|
const keyID = process.env.APPLE_KEY_ID || "TB3V355G5Y";
|
||||||
|
const appleAppID = process.env.APPLE_APP_ID;
|
||||||
|
|
||||||
|
// Token expiration
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const exp = now + 86400 * 180; // 6 months
|
||||||
|
|
||||||
|
const claims = {
|
||||||
|
iss: teamID,
|
||||||
|
iat: now,
|
||||||
|
exp: exp,
|
||||||
|
aud: 'https://appleid.apple.com',
|
||||||
|
sub: appleAppID,
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = jwt.sign(claims, appleKey, { algorithm: 'ES256', header: { alg: 'ES256', kid: keyID } });
|
||||||
|
console.log("generated new token:" + token);
|
||||||
|
return token;
|
||||||
|
}
|
42
pages/api/auth/apple-token.ts
Normal file
42
pages/api/auth/apple-token.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// pages/api/auth/apple-token.js
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
const dotenv = require("dotenv");
|
||||||
|
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const appleKey = fs.readFileSync(path.resolve('./_deploy/appleKey.p8'), 'utf8');
|
||||||
|
const teamID = process.env.APPLE_TEAM_ID || "XC57P9SXDK";
|
||||||
|
const keyID = process.env.APPLE_KEY_ID || "TB3V355G5Y";
|
||||||
|
const appleAppID = process.env.APPLE_APP_ID || "com.mwitnessing.mwitnessing";
|
||||||
|
const token = jwt.sign({}, appleKey, {
|
||||||
|
algorithm: 'ES256',
|
||||||
|
expiresIn: '180d',
|
||||||
|
issuer: teamID,
|
||||||
|
header: {
|
||||||
|
alg: 'ES256',
|
||||||
|
kid: keyID,
|
||||||
|
},
|
||||||
|
audience: 'https://appleid.apple.com',
|
||||||
|
subject: appleAppID,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to Apple's authentication page, or send the token to the client to do so
|
||||||
|
console.log(token);
|
||||||
|
res.status(200).send({
|
||||||
|
message: 'Generated token for Apple Sign In',
|
||||||
|
token: token
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error signing in with Apple:', error);
|
||||||
|
res.status(500).send({ error: 'Failed to sign in with Apple' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle any non-GET requests
|
||||||
|
res.setHeader('Allow', ['GET']);
|
||||||
|
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||||
|
}
|
||||||
|
}
|
40
pages/api/auth/login-as.js
Normal file
40
pages/api/auth/login-as.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// pages/api/auth/login-as.js
|
||||||
|
|
||||||
|
import { getSession } from "next-auth/react";
|
||||||
|
import prisma from '../../../lib/prisma'; // Adjust the path as per your setup
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const session = await getSession({ req });
|
||||||
|
if (session && session.user.role === 'admin') {
|
||||||
|
const { userId } = req.body;
|
||||||
|
|
||||||
|
const userToImpersonate = await prisma.publisher.findUnique({
|
||||||
|
where: { id: userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userToImpersonate) {
|
||||||
|
// Create a custom session object for the impersonated user
|
||||||
|
const impersonatedSession = {
|
||||||
|
...session,
|
||||||
|
user: {
|
||||||
|
...session.user,
|
||||||
|
id: userToImpersonate.id,
|
||||||
|
email: userToImpersonate.email,
|
||||||
|
name: userToImpersonate.name,
|
||||||
|
role: userToImpersonate.role,
|
||||||
|
// add other necessary fields
|
||||||
|
},
|
||||||
|
impersonating: true, // flag to indicate impersonation
|
||||||
|
originalUser: session.user // save the original user for later
|
||||||
|
};
|
||||||
|
|
||||||
|
// Here you would typically use some method to create a session server-side
|
||||||
|
// For this example, we'll just send the impersonated session as a response
|
||||||
|
res.status(200).json({ session: impersonatedSession });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(403).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
}
|
0
pages/api/content.ts
Normal file
0
pages/api/content.ts
Normal file
145
pages/api/content/[subfolder].ts
Normal file
145
pages/api/content/[subfolder].ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import nc from 'next-connect';
|
||||||
|
|
||||||
|
const handler = nc({
|
||||||
|
onError: (err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).end('Something broke!');
|
||||||
|
},
|
||||||
|
onNoMatch: (req, res) => {
|
||||||
|
res.status(404).end('Page is not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.use((req: NextApiRequest, res: NextApiResponse, next) => {
|
||||||
|
const subfolder = req.query.subfolder as string;
|
||||||
|
const upload = createUploadMiddleware(subfolder).array('image');
|
||||||
|
upload(req, res, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Failed to upload files.', details: err.message });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.post((req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Process uploaded files
|
||||||
|
// Example response
|
||||||
|
res.json({ message: 'Files uploaded successfully', files: req.files });
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.get((req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Handle listing files
|
||||||
|
//listFiles(req, res, req.subfolder);
|
||||||
|
listFiles(req, res, req.query.subfolder as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.delete((req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Handle deleting files
|
||||||
|
deleteFile(req, res, req.query.subfolder as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
//handling file uploads
|
||||||
|
import multer from 'multer';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
// Generalized Multer configuration
|
||||||
|
export const createUploadMiddleware = (folder: string) => {
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const uploadPath = path.join(process.cwd(), 'public/content', folder);
|
||||||
|
if (!fs.existsSync(uploadPath)) {
|
||||||
|
fs.mkdirSync(uploadPath, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, uploadPath);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const prefix = req.body.prefix || path.parse(file.originalname).name;
|
||||||
|
cb(null, `${prefix}${path.extname(file.originalname)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return multer({ storage });
|
||||||
|
};
|
||||||
|
|
||||||
|
async function processFiles(req, res, folder) {
|
||||||
|
if (!req.files || req.files.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No files uploaded.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadDir = path.join(process.cwd(), 'public/content', folder);
|
||||||
|
const thumbDir = path.join(uploadDir, "thumb");
|
||||||
|
|
||||||
|
if (!fs.existsSync(thumbDir)) {
|
||||||
|
fs.mkdirSync(thumbDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const processedFiles = await Promise.all(req.files.map(async (file) => {
|
||||||
|
const originalPath = path.join(uploadDir, file.filename);
|
||||||
|
const thumbPath = path.join(thumbDir, file.filename);
|
||||||
|
|
||||||
|
await sharp(file.path)
|
||||||
|
.resize({ width: 1920, fit: sharp.fit.inside, withoutEnlargement: true })
|
||||||
|
.jpeg({ quality: 80 })
|
||||||
|
.toFile(originalPath);
|
||||||
|
|
||||||
|
await sharp(file.path)
|
||||||
|
.resize(320, 320, { fit: sharp.fit.inside, withoutEnlargement: true })
|
||||||
|
.toFile(thumbPath);
|
||||||
|
|
||||||
|
fs.unlinkSync(file.path); // Remove temp file
|
||||||
|
|
||||||
|
return {
|
||||||
|
originalUrl: `/content/${folder}/${file.filename}`,
|
||||||
|
thumbUrl: `/content/${folder}/thumb/${file.filename}`
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(processedFiles);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing files:', error);
|
||||||
|
res.status(500).json({ error: 'Error processing files.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List files in a directory
|
||||||
|
async function listFiles(req, res, folder) {
|
||||||
|
const directory = path.join(process.cwd(), 'public/content', folder);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await fs.promises.readdir(directory);
|
||||||
|
const imageUrls = files.map(file => `${req.protocol}://${req.get('host')}/content/${folder}/${file}`);
|
||||||
|
res.json({ imageUrls });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error reading uploads directory:', err);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a file
|
||||||
|
async function deleteFile(req, res, folder) {
|
||||||
|
const filename = req.query.file;
|
||||||
|
if (!filename) {
|
||||||
|
return res.status(400).send('Filename is required.');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const filePath = path.join(process.cwd(), 'public/content', folder, filename);
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
res.status(200).send('File deleted successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send('Failed to delete the file.');
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
//Find the absolute path of the json directory and the requested file contents
|
|
||||||
const jsonDirectory = path.join(process.cwd(), 'content');
|
|
||||||
const requestedFile = req.query.nextcrud[0];
|
|
||||||
const fileContents = await fs.readFile(path.join(jsonDirectory, requestedFile), 'utf8');
|
|
||||||
// try to determine the content type from the file extension
|
|
||||||
const contentType = requestedFile.endsWith('.json') ? 'application/json' : 'text/plain';
|
|
||||||
// return the file contents with the appropriate content type
|
|
||||||
res.status(200).setHeader('Content-Type', contentType).end(fileContents);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -4,9 +4,11 @@ import { getToken } from "next-auth/jwt";
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { createRouter, expressWrapper } from "next-connect";
|
import { createRouter, expressWrapper } from "next-connect";
|
||||||
const common = require('../../src/helpers/common');
|
const common = require('../../src/helpers/common');
|
||||||
|
const data = require('../../src/helpers/data');
|
||||||
const emailHelper = require('../../src/helpers/email');
|
const emailHelper = require('../../src/helpers/email');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const CON = require("../../src/helpers/const");
|
const CON = require("../../src/helpers/const");
|
||||||
|
import { EventLogType } from "@prisma/client";
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@ -127,6 +129,15 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await prisma.eventLog.create({
|
||||||
|
data: {
|
||||||
|
date: new Date(),
|
||||||
|
publisher: { connect: { id: publisher.id } },
|
||||||
|
shift: { connect: { id: assignment.shiftId } },
|
||||||
|
type: EventLogType.AssignmentReplacementAccepted,
|
||||||
|
content: "Заявка за заместване приета от " + publisher.firstName + " " + publisher.lastName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
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 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)}`;
|
||||||
@ -201,7 +212,7 @@ export default async function handler(req, res) {
|
|||||||
return res.status(401).json({ message: "Unauthorized to call this API endpoint" });
|
return res.status(401).json({ message: "Unauthorized to call this API endpoint" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.publisher.findUnique({
|
const publisher = await prisma.publisher.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email: token.email
|
email: token.email
|
||||||
}
|
}
|
||||||
@ -209,13 +220,14 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "sendCoverMeRequestByEmail":
|
case "sendCoverMeRequestByEmail":
|
||||||
// Send CoverMe request to the user
|
// Send CoverMe request to the users
|
||||||
//get from POST data: shiftId, assignmentId, date
|
//get from POST data: shiftId, assignmentId, date
|
||||||
//let shiftId = req.body.shiftId;
|
//let shiftId = req.body.shiftId;
|
||||||
let assignmentId = req.body.assignmentId;
|
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 toSubscribed = req.body.toSubscribed;
|
||||||
|
let toAvailable = req.body.toAvailable;
|
||||||
|
|
||||||
|
|
||||||
let assignment = await prisma.assignment.findUnique({
|
let assignment = await prisma.assignment.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -233,6 +245,8 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log("User: " + publisher.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " - " + assignment.shift.cartEvent.location.name + " " + assignment.shift.startTime.toISOString());
|
||||||
|
|
||||||
|
|
||||||
// update the assignment. generate new publicGuid, isConfirmed to false
|
// update the assignment. generate new publicGuid, isConfirmed to false
|
||||||
let newPublicGuid = uuidv4();
|
let newPublicGuid = uuidv4();
|
||||||
@ -246,29 +260,55 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//get all subscribed publisers
|
let subscribedPublishers = [], availablePublishers = [];
|
||||||
const subscribedPublishers = await prisma.publisher.findMany({
|
if (toSubscribed) {
|
||||||
where: {
|
//get all subscribed publisers
|
||||||
isSubscribedToCoverMe: true
|
subscribedPublishers = await prisma.publisher.findMany({
|
||||||
|
where: {
|
||||||
|
isSubscribedToCoverMe: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (toAvailable) {
|
||||||
|
availablePublishers = await data.filterPublishersNew("id,firstName,lastName,email", new Date(assignment.shift.startTime), true, false);
|
||||||
|
|
||||||
|
}
|
||||||
|
//concat and remove duplicate emails
|
||||||
|
let pubsToSend = subscribedPublishers.concat(availablePublishers).
|
||||||
|
filter((item, index, self) =>
|
||||||
|
index === self.findIndex((t) => (
|
||||||
|
t.email === item.email && item.email !== publisher.email//and exclude the user himself
|
||||||
|
))
|
||||||
|
);
|
||||||
|
console.log("Sending CoverMe request to " + pubsToSend.length + " publishers");
|
||||||
|
|
||||||
|
await prisma.eventLog.create({
|
||||||
|
data: {
|
||||||
|
date: new Date(),
|
||||||
|
publisher: { connect: { id: publisher.id } },
|
||||||
|
shift: { connect: { id: assignment.shiftId } },
|
||||||
|
type: EventLogType.AssignmentReplacementRequested,
|
||||||
|
content: "Заявка за заместване от " + publisher.firstName + " " + publisher.lastName
|
||||||
|
+ "до: " + pubsToSend.map(p => p.firstName + " " + p.lastName + "<" + p.email + ">").join(", "),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//send email to all subscribed publishers
|
//send email to all subscribed publishers
|
||||||
for (let i = 0; i < subscribedPublishers.length; i++) {
|
for (let i = 0; i < pubsToSend.length; i++) {
|
||||||
if (subscribedPublishers[i].id == user.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//send email to subscribed publisher
|
//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 acceptUrl = process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=coverMeAccept&userId=" + pubsToSend[i].id + "&shiftId=" + assignment.shiftId + "&assignmentPID=" + newPublicGuid;
|
||||||
|
publisher.prefix = publisher.isMale ? "Брат" : "Сестра";
|
||||||
|
|
||||||
let model = {
|
let model = {
|
||||||
user: user,
|
user: publisher,
|
||||||
shiftId: assignment.shiftId,
|
shiftId: assignment.shiftId,
|
||||||
acceptUrl: acceptUrl,
|
acceptUrl: acceptUrl,
|
||||||
prefix: user.isMale ? "Брат" : "Сестра",
|
firstName: pubsToSend[i].firstName,
|
||||||
firstName: subscribedPublishers[i].firstName,
|
lastName: pubsToSend[i].lastName,
|
||||||
lastName: subscribedPublishers[i].lastName,
|
email: pubsToSend[i].email,
|
||||||
email: subscribedPublishers[i].email,
|
|
||||||
placeName: assignment.shift.cartEvent.location.name,
|
placeName: assignment.shift.cartEvent.location.name,
|
||||||
dateStr: common.getDateFormated(assignment.shift.startTime),
|
dateStr: common.getDateFormated(assignment.shift.startTime),
|
||||||
time: common.formatTimeHHmm(assignment.shift.startTime),
|
time: common.formatTimeHHmm(assignment.shift.startTime),
|
||||||
@ -276,8 +316,8 @@ export default async function handler(req, res) {
|
|||||||
};
|
};
|
||||||
let results = emailHelper.SendEmailHandlebars(
|
let results = emailHelper.SendEmailHandlebars(
|
||||||
{
|
{
|
||||||
name: subscribedPublishers[i].firstName + " " + subscribedPublishers[i].lastName,
|
name: pubsToSend[i].firstName + " " + pubsToSend[i].lastName,
|
||||||
email: subscribedPublishers[i].email
|
email: pubsToSend[i].email
|
||||||
}, "coverMe", model);
|
}, "coverMe", model);
|
||||||
// if (results) {
|
// if (results) {
|
||||||
// console.log("Error sending email: " + error);
|
// console.log("Error sending email: " + error);
|
||||||
@ -285,10 +325,12 @@ export default async function handler(req, res) {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
console.log("Email sent to: " + subscribedPublishers[i].email);
|
console.log("Email sent to: " + pubsToSend[i].email);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
res.status(200).json({ message: "CoverMe request sent" });
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return res.status(400).json({ message: "Invalid action" });
|
return res.status(400).json({ message: "Invalid action" });
|
||||||
|
@ -30,7 +30,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
var action = req.query.action;
|
var action = req.query.action;
|
||||||
var filter = req.query.filter;
|
var filter = req.query.filter;
|
||||||
let day: Date, monthInfo: any;
|
let day: Date;
|
||||||
let isExactTime;
|
let isExactTime;
|
||||||
if (req.query.date) {
|
if (req.query.date) {
|
||||||
day = new Date(req.query.date);
|
day = new Date(req.query.date);
|
||||||
@ -42,6 +42,7 @@ export default async function handler(req, res) {
|
|||||||
isExactTime = true;
|
isExactTime = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let monthInfo = common.getMonthDatesInfo(day);
|
||||||
const searchText = req.query.searchText?.normalize('NFC');
|
const searchText = req.query.searchText?.normalize('NFC');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -123,10 +124,10 @@ export default async function handler(req, res) {
|
|||||||
const availabilities = req.body;
|
const availabilities = req.body;
|
||||||
//! console.log("createAvailabilities: " + JSON.stringify(availabilities));
|
//! console.log("createAvailabilities: " + JSON.stringify(availabilities));
|
||||||
try {
|
try {
|
||||||
await prisma.availability.createMany({
|
let createResults = await prisma.availability.createMany({
|
||||||
data: availabilities
|
data: availabilities
|
||||||
});
|
});
|
||||||
res.status(200).json({ "message": "ok" });
|
res.status(200).json({ "message": "ok", "results": createResults });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating availabilities: " + error);
|
console.error("Error creating availabilities: " + error);
|
||||||
res.status(500).json({ error });
|
res.status(500).json({ error });
|
||||||
@ -161,8 +162,9 @@ export default async function handler(req, res) {
|
|||||||
res.status(200).json(publishers);
|
res.status(200).json(publishers);
|
||||||
break;
|
break;
|
||||||
case "filterPublishersNew":
|
case "filterPublishersNew":
|
||||||
|
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
|
||||||
let results = await filterPublishersNew_Available(req.query.select, day,
|
let results = await filterPublishersNew_Available(req.query.select, day,
|
||||||
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth));
|
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities);
|
||||||
res.status(200).json(results);
|
res.status(200).json(results);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -219,6 +221,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
res.status(200).json(shiftsForDate);
|
res.status(200).json(shiftsForDate);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "copyOldAvailabilities":
|
case "copyOldAvailabilities":
|
||||||
//get all publishers that don't have availabilities for the current month
|
//get all publishers that don't have availabilities for the current month
|
||||||
monthInfo = common.getMonthDatesInfo(day);
|
monthInfo = common.getMonthDatesInfo(day);
|
||||||
@ -282,12 +285,12 @@ export default async function handler(req, res) {
|
|||||||
type: AvailabilityType.Monthly,
|
type: AvailabilityType.Monthly,
|
||||||
isFromPreviousMonth: true,
|
isFromPreviousMonth: true,
|
||||||
name: avail.name || "старо предпочитание",
|
name: avail.name || "старо предпочитание",
|
||||||
// parentAvailabilityId: avail.id
|
parentAvailabilityId: avail.id,
|
||||||
parentAvailability: {
|
// parentAvailability: {
|
||||||
connect: {
|
// connect: {
|
||||||
id: avail.id
|
// id: avail.id
|
||||||
}
|
// }
|
||||||
}
|
// },
|
||||||
}
|
}
|
||||||
await prisma.availability.create({ data: data });
|
await prisma.availability.create({ data: data });
|
||||||
|
|
||||||
@ -330,12 +333,11 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
case "updateShifts":
|
case "updateShifts":
|
||||||
//get all shifts for the month and publish them (we pass date )
|
//get all shifts for the month and publish them (we pass date )
|
||||||
let monthInfo = common.getMonthDatesInfo(day);
|
|
||||||
let isPublished = common.parseBool(req.query.isPublished);
|
let isPublished = common.parseBool(req.query.isPublished);
|
||||||
let updated = await prisma.shift.updateMany({
|
let updated = await prisma.shift.updateMany({
|
||||||
where: {
|
where: {
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: new Date(monthInfo.firstMonday.getFullYear(), monthInfo.firstMonday.getMonth(), 1),
|
gte: new Date(monthInfo.firstMonday.getFullYear(), monthInfo.firstMonday.getMonth(), monthInfo.firstMonday.getDate()),
|
||||||
lt: new Date(monthInfo.lastSunday.getFullYear(), monthInfo.lastSunday.getMonth() + 1, 1),
|
lt: new Date(monthInfo.lastSunday.getFullYear(), monthInfo.lastSunday.getMonth() + 1, 1),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -347,6 +349,43 @@ export default async function handler(req, res) {
|
|||||||
res.status(200).json({ "message": "ok" });
|
res.status(200).json({ "message": "ok" });
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case "getPossibleShiftPublisherEmails":
|
||||||
|
const subscribedPublishers = await prisma.publisher.findMany({
|
||||||
|
where: {
|
||||||
|
isSubscribedToCoverMe: true
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
}).then(pubs => {
|
||||||
|
return pubs.map(pub => {
|
||||||
|
return {
|
||||||
|
id: pub.id,
|
||||||
|
name: pub.firstName + " " + pub.lastName,
|
||||||
|
email: pub.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let shift = await prisma.shift.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.query.shiftId)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let availablePublishers = await filterPublishersNew_Available("id,firstName,lastName,email", new Date(shift.startTime), true, false);
|
||||||
|
//return names and email info only
|
||||||
|
availablePublishers = availablePublishers.map(pub => {
|
||||||
|
return {
|
||||||
|
id: pub.id,
|
||||||
|
name: pub.firstName + " " + pub.lastName,
|
||||||
|
email: pub.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.status(200).json({ shift, availablePublishers: availablePublishers, subscribedPublishers });
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
@ -424,255 +463,8 @@ export async function getMonthlyStatistics(selectFields, filterDate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true) {
|
export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true, includeOldAvailabilities = false) {
|
||||||
|
return data.filterPublishersNew(selectFields, filterDate, isExactTime, isForTheMonth, isWithStats, includeOldAvailabilities);
|
||||||
// Only attempt to split if selectFields is a string; otherwise, use it as it is.
|
|
||||||
selectFields = typeof selectFields === 'string' ? selectFields.split(",") : selectFields;
|
|
||||||
|
|
||||||
let selectBase = selectFields.reduce((acc, curr) => {
|
|
||||||
acc[curr] = true;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
selectBase.assignments = {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
shift: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
shift: {
|
|
||||||
startTime: {
|
|
||||||
gte: filterDate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var monthInfo = common.getMonthDatesInfo(filterDate);
|
|
||||||
var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber
|
|
||||||
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate);
|
|
||||||
if (!isExactTime) {
|
|
||||||
filterDate.setHours(0, 0, 0, 0); // Set to midnight
|
|
||||||
}
|
|
||||||
const filterDateEnd = new Date(filterDate);
|
|
||||||
filterDateEnd.setHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
|
|
||||||
let whereClause = {};
|
|
||||||
//if full day, match by date only
|
|
||||||
if (!isExactTime) { // Check only by date without considering time ( Assignments on specific days without time)
|
|
||||||
whereClause["availabilities"] = {
|
|
||||||
some: {
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
startTime: { gte: filterDate },
|
|
||||||
endTime: { lte: filterDateEnd },
|
|
||||||
}
|
|
||||||
,
|
|
||||||
// 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, // includes monthly and weekly repeats
|
|
||||||
dayofweek: dayOfWeekEnum,
|
|
||||||
// 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: [
|
|
||||||
// Check if dayOfMonth is set and filterDate is between start and end dates (Assignments on specific days AND time)
|
|
||||||
{
|
|
||||||
// dayOfMonth: filterDate.getDate(),
|
|
||||||
startTime: { lte: filterDate },
|
|
||||||
endTime: { gte: filterDate }
|
|
||||||
},
|
|
||||||
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
|
||||||
{
|
|
||||||
dayOfMonth: null,
|
|
||||||
dayofweek: dayOfWeekEnum,
|
|
||||||
startTime: { gte: filterDate },
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
|
||||||
{ endDate: { gte: filterDate } },
|
|
||||||
{ endDate: null }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (isForTheMonth) {
|
|
||||||
// If no filter date, return all publishers's availabilities for currentMonthStart
|
|
||||||
whereClause["availabilities"] = {
|
|
||||||
some: {
|
|
||||||
OR: [
|
|
||||||
// Check if dayOfMonth is not null and startTime is after currentMonthStart (Assignments on specific days AND time)
|
|
||||||
{
|
|
||||||
dayOfMonth: { not: null },
|
|
||||||
startTime: { gte: currentMonthStart },
|
|
||||||
endTime: { lte: currentMonthEnd }
|
|
||||||
},
|
|
||||||
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
|
||||||
{
|
|
||||||
dayOfMonth: null,
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
|
||||||
{ endDate: { gte: filterDate } },
|
|
||||||
{ endDate: null }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`getting publishers for date: ${filterDate}, isExactTime: ${isExactTime}, isForTheMonth: ${isForTheMonth}`);
|
|
||||||
//include availabilities if flag is true
|
|
||||||
const prisma = common.getPrismaClient(); //why we need to get it again?
|
|
||||||
let publishers = await prisma.publisher.findMany({
|
|
||||||
where: whereClause,
|
|
||||||
select: {
|
|
||||||
...selectBase,
|
|
||||||
availabilities: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`);
|
|
||||||
|
|
||||||
// convert matching weekly availabilities to availabilities for the day to make furter processing easier on the client.
|
|
||||||
// we trust that the filtering was OK, so we use the dateFilter as date.
|
|
||||||
publishers.forEach(pub => {
|
|
||||||
pub.availabilities = pub.availabilities.map(avail => {
|
|
||||||
if (avail.dayOfMonth == null) {
|
|
||||||
let newStart = new Date(filterDate);
|
|
||||||
newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
|
|
||||||
let newEnd = new Date(filterDate);
|
|
||||||
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
|
|
||||||
return {
|
|
||||||
...avail,
|
|
||||||
startTime: newStart,
|
|
||||||
endTime: newEnd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return avail;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let currentWeekStart: Date, currentWeekEnd: Date,
|
|
||||||
currentMonthStart: Date, currentMonthEnd: Date,
|
|
||||||
previousMonthStart: Date, previousMonthEnd: Date;
|
|
||||||
|
|
||||||
if (isWithStats) {
|
|
||||||
currentWeekStart = common.getStartOfWeek(filterDate);
|
|
||||||
currentWeekEnd = common.getEndOfWeek(filterDate);
|
|
||||||
currentMonthStart = monthInfo.firstMonday;
|
|
||||||
currentMonthEnd = monthInfo.lastSunday;
|
|
||||||
let prevMnt = new Date(filterDate)
|
|
||||||
prevMnt.setMonth(prevMnt.getMonth() - 1);
|
|
||||||
monthInfo = common.getMonthDatesInfo(prevMnt);
|
|
||||||
previousMonthStart = monthInfo.firstMonday;
|
|
||||||
previousMonthEnd = monthInfo.lastSunday;
|
|
||||||
|
|
||||||
//get if publisher has assignments for current weekday, week, current month, previous month
|
|
||||||
publishers.forEach(pub => {
|
|
||||||
// Filter assignments for current day
|
|
||||||
pub.currentDayAssignments = pub.assignments?.filter(assignment => {
|
|
||||||
return assignment.shift.startTime >= filterDate && assignment.shift.startTime <= filterDateEnd;
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
// Filter assignments for current week
|
|
||||||
pub.currentWeekAssignments = pub.assignments?.filter(assignment => {
|
|
||||||
return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd;
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
// Filter assignments for current month
|
|
||||||
pub.currentMonthAssignments = pub.assignments?.filter(assignment => {
|
|
||||||
return assignment.shift.startTime >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd;
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
// Filter assignments for previous month
|
|
||||||
pub.previousMonthAssignments = pub.assignments?.filter(assignment => {
|
|
||||||
return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd;
|
|
||||||
}).length;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the availabilities for the day. Calcullate:
|
|
||||||
//1. how many days the publisher is available for the current month - only with dayOfMonth
|
|
||||||
//2. how many days the publisher is available without dayOfMonth (previous months count)
|
|
||||||
//3. how many hours in total the publisher is available for the current month
|
|
||||||
publishers.forEach(pub => {
|
|
||||||
if (isWithStats) {
|
|
||||||
pub.currentMonthAvailability = pub.availabilities?.filter(avail => {
|
|
||||||
// return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
|
|
||||||
return avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
|
|
||||||
})
|
|
||||||
pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length || 0;
|
|
||||||
// pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => {
|
|
||||||
// return avail.dayOfMonth == null;
|
|
||||||
// }).length;
|
|
||||||
pub.currentMonthAvailabilityHoursCount = pub.currentMonthAvailability.reduce((acc, curr) => {
|
|
||||||
return acc + (curr.endTime.getTime() - curr.startTime.getTime()) / (1000 * 60 * 60);
|
|
||||||
}, 0);
|
|
||||||
//if pub has up-to-date availabilities (with dayOfMonth) for the current month
|
|
||||||
pub.hasUpToDateAvailabilities = pub.availabilities?.some(avail => {
|
|
||||||
return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//if pub has ever filled the form - if has availabilities which are not from previous assignments
|
|
||||||
pub.hasEverFilledForm = pub.availabilities?.some(avail => {
|
|
||||||
return avail.isFromPreviousAssignments == false;
|
|
||||||
});
|
|
||||||
|
|
||||||
//if pub has availabilities for the current day
|
|
||||||
pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => {
|
|
||||||
return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isExactTime) {
|
|
||||||
// Post filter for time if dayOfMonth is null as we can't only by time for multiple dates in SQL
|
|
||||||
// Modify the availabilities array of the filtered publishers
|
|
||||||
publishers.forEach(pub => {
|
|
||||||
pub.availabilities = pub.availabilities?.filter(avail => matchesAvailability(avail, filterDate));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return publishers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// availabilites filter:
|
// availabilites filter:
|
||||||
@ -759,7 +551,8 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true
|
endTime: true,
|
||||||
|
isPublished: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1062,7 +855,11 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (assignments) {
|
if (assignments) {
|
||||||
publisher.assignments?.forEach(item => {
|
//only published shifts
|
||||||
|
|
||||||
|
publisher.assignments?.filter(
|
||||||
|
assignment => assignment.shift.isPublished
|
||||||
|
).forEach(item => {
|
||||||
result.push({
|
result.push({
|
||||||
...item,
|
...item,
|
||||||
title: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
|
title: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
|
||||||
|
37
pages/api/notify.ts
Normal file
37
pages/api/notify.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
const webPush = require('web-push')
|
||||||
|
|
||||||
|
webPush.setVapidDetails(
|
||||||
|
`mailto:${process.env.WEB_PUSH_EMAIL}`,
|
||||||
|
process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY,
|
||||||
|
process.env.WEB_PUSH_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
const Notification = (req, res) => {
|
||||||
|
if (req.method == 'POST') {
|
||||||
|
const { subscription } = req.body
|
||||||
|
|
||||||
|
webPush
|
||||||
|
.sendNotification(
|
||||||
|
subscription,
|
||||||
|
JSON.stringify({ title: 'Hello Web Push', message: 'Your web push notification is here!' })
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
res.writeHead(response.statusCode, response.headers).end(response.body)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if ('statusCode' in err) {
|
||||||
|
res.writeHead(err.statusCode, err.headers).end(err.body)
|
||||||
|
} else {
|
||||||
|
console.error(err)
|
||||||
|
res.statusCode = 500
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.statusCode = 405
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification
|
@ -73,17 +73,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
// const { year, month } = req.query;
|
const { year, month } = req.query;
|
||||||
|
|
||||||
// let monthIndex = parseInt(month as string) - 1;
|
//ToDo: maybe we don't need that anymore as we are publishing the shifts and show all published shifts
|
||||||
// const monthInfo = common.getMonthDatesInfo(new Date(year, month, 1));
|
|
||||||
// let fromDate = monthInfo.firstMonday;
|
|
||||||
// const toDate = monthInfo.lastSunday;
|
|
||||||
let fromDate = new Date();
|
let fromDate = new Date();
|
||||||
fromDate.setDate(fromDate.getDate() - 1);
|
fromDate.setDate(fromDate.getDate() - 1);
|
||||||
fromDate.setHours(0, 0, 0, 0);
|
fromDate.setHours(0, 0, 0, 0);
|
||||||
let toDate = new Date(fromDate);
|
if (year && month) {
|
||||||
toDate.setDate(toDate.getDate() + 40);
|
fromDate = new Date(parseInt(year as string), parseInt(month as string) - 1, 1);
|
||||||
|
}
|
||||||
|
const monthInfo = common.getMonthDatesInfo(fromDate);
|
||||||
|
//let toDate = new Date(monthInfo.lastSunday);
|
||||||
|
if (year && month) {
|
||||||
|
fromDate = monthInfo.firstMonday;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@ -93,7 +96,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
isPublished: true,
|
isPublished: true,
|
||||||
startTime: {
|
startTime: {
|
||||||
gte: fromDate,
|
gte: fromDate,
|
||||||
lt: toDate,
|
//lt: toDate,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
@ -131,12 +134,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
groupedShifts[day][time] = [];
|
groupedShifts[day][time] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let { notes, notes_bold } = splitNotes(shift.notes);//.substring(Math.max(shift.notes.lastIndexOf("-"), shift.notes.lastIndexOf("–")) + 1).trim() || "";
|
// let { notes, notes_bold } = splitNotes(shift.notes);//.substring(Math.max(shift.notes.lastIndexOf("-"), shift.notes.lastIndexOf("–")) + 1).trim() || "";
|
||||||
|
let notes = "", notes_bold = "";
|
||||||
|
|
||||||
|
if (shift.assignments.some(a => a.isWithTransport)) {
|
||||||
|
if (shift.requiresTransport) {
|
||||||
|
notes = "Транспорт: ";
|
||||||
|
notes_bold = " " + shift.assignments.filter(a => a.isWithTransport).map(a => common.getInitials(a.publisher.firstName + " " + a.publisher.lastName)).join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let shiftSchedule = {
|
let shiftSchedule = {
|
||||||
date: date,
|
date: date,
|
||||||
placeOfEvent: shift.cartEvent.location.name,
|
placeOfEvent: shift.cartEvent.location.name,
|
||||||
time: time,
|
time: time,
|
||||||
|
requiresTransport: shift.requiresTransport,
|
||||||
//bold the text after - in the notes
|
//bold the text after - in the notes
|
||||||
notes: notes,
|
notes: notes,
|
||||||
notes_bold: notes_bold,
|
notes_bold: notes_bold,
|
||||||
@ -159,6 +171,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
console.log(err + " " + JSON.stringify(shifts[i]));
|
console.log(err + " " + JSON.stringify(shifts[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const day in groupedShifts) {
|
||||||
|
const times = Object.keys(groupedShifts[day]);
|
||||||
|
for (const time of times) {
|
||||||
|
const shift = groupedShifts[day][time][0];
|
||||||
|
if (shift) {
|
||||||
|
// Determine the first shift of the day if it requires transport
|
||||||
|
if (time === times[0] && shift.requiresTransport) { // Check if this is the first time slot of the day
|
||||||
|
shift.notes = "Докарва количка от Люлин -"; // Update the first shift in the first time slot
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the last shift of the day if it requires transport
|
||||||
|
if (time === times[times.length - 1] && shift.requiresTransport) { // Check if this is the last time slot of the day
|
||||||
|
shift.notes = "Прибира количка в Люлин -"; // Update the last shift in the last time slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the output object in the format of the second JSON file
|
// Create the output object in the format of the second JSON file
|
||||||
const monthlySchedule = {
|
const monthlySchedule = {
|
||||||
month: common.getMonthName(shifts[0].startTime.getMonth()),
|
month: common.getMonthName(shifts[0].startTime.getMonth()),
|
||||||
@ -176,6 +206,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
console.log("shift is null");
|
console.log("shift is null");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let weekday = common.getDayOfWeekName(shift.date);
|
let weekday = common.getDayOfWeekName(shift.date);
|
||||||
let monthName = common.getMonthName(shift.date.getMonth());
|
let monthName = common.getMonthName(shift.date.getMonth());
|
||||||
weekday = weekday.charAt(0).toUpperCase() + weekday.slice(1);
|
weekday = weekday.charAt(0).toUpperCase() + weekday.slice(1);
|
||||||
|
@ -76,15 +76,15 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
const [modalPub, setModalPub] = useState(null);
|
const [modalPub, setModalPub] = useState(null);
|
||||||
|
|
||||||
// ------------------ no assignments checkbox ------------------
|
// ------------------ no assignments checkbox ------------------
|
||||||
const [isCheckboxChecked, setIsCheckboxChecked] = useState(false);
|
const [filterShowWithoutAssignments, setFilterShowWithoutAssignments] = useState(false);
|
||||||
const handleCheckboxChange = (event) => {
|
const handleCheckboxChange = (event) => {
|
||||||
setIsCheckboxChecked(!isCheckboxChecked); // Toggle the checkbox state
|
setFilterShowWithoutAssignments(!filterShowWithoutAssignments); // Toggle the checkbox state
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("checkbox checked: " + isCheckboxChecked);
|
console.log("checkbox checked: " + filterShowWithoutAssignments);
|
||||||
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
|
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
|
||||||
}, [isCheckboxChecked]); // Dependency array
|
}, [filterShowWithoutAssignments]); // Dependency array
|
||||||
|
|
||||||
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
|
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -99,7 +99,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
var date = new Date(common.getDateFromDateTime(selectedDate));//ToDo: check if seting the timezone affects the selectedDate?!
|
var date = new Date(common.getDateFromDateTime(selectedDate));//ToDo: check if seting the timezone affects the selectedDate?!
|
||||||
var dateStr = common.getISODateOnly(date);
|
var dateStr = common.getISODateOnly(date);
|
||||||
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
|
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
|
||||||
if (isCheckboxChecked) {
|
if (filterShowWithoutAssignments) {
|
||||||
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
|
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
|
||||||
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
||||||
setAvailablePubs(availablePubsForDate);
|
setAvailablePubs(availablePubsForDate);
|
||||||
@ -110,7 +110,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
|
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
|
||||||
setShifts(shiftsForDate);
|
setShifts(shiftsForDate);
|
||||||
setIsPublished(shiftsForDate.some(shift => shift.isPublished));
|
setIsPublished(shiftsForDate.some(shift => shift.isPublished));
|
||||||
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishersNew&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishersNew&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth&includeOldAvailabilities=${filterShowWithoutAssignments}`);
|
||||||
|
|
||||||
availablePubsForDate.forEach(pub => {
|
availablePubsForDate.forEach(pub => {
|
||||||
pub.canTransport = pub.availabilities.some(av =>
|
pub.canTransport = pub.availabilities.some(av =>
|
||||||
@ -140,6 +140,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
onChange(selectedDate);
|
onChange(selectedDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShiftSelection = (selectedShift) => {
|
const handleShiftSelection = (selectedShift) => {
|
||||||
setSelectedShiftId(selectedShift.id);
|
setSelectedShiftId(selectedShift.id);
|
||||||
const updatedPubs = availablePubs.map(pub => {
|
const updatedPubs = availablePubs.map(pub => {
|
||||||
@ -535,6 +536,38 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
|
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCreateNewShift(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||||||
|
//get last shift end time
|
||||||
|
let lastShift = shifts.sort((a, b) => new Date(b.endTime).getTime() - new Date(a.endTime).getTime())[0];
|
||||||
|
//default to 9:00 if no shifts
|
||||||
|
if (!lastShift) {
|
||||||
|
//get cart event id
|
||||||
|
var dayName = common.DaysOfWeekArray[value.getDayEuropean()];
|
||||||
|
const cartEvent = events.find(event => event.dayofweek == dayName);
|
||||||
|
lastShift = {
|
||||||
|
endTime: new Date(value.setHours(9, 0, 0, 0)),
|
||||||
|
cartEventId: cartEvent.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const lastShiftEndTime = new Date(lastShift.endTime);
|
||||||
|
//add 90 minutes
|
||||||
|
const newShiftEndTime = new Date(lastShiftEndTime.getTime() + 90 * 60000);
|
||||||
|
await axiosInstance.post(`/api/data/shifts`, {
|
||||||
|
name: "Нова смяна",
|
||||||
|
startTime: lastShiftEndTime,
|
||||||
|
endTime: newShiftEndTime,
|
||||||
|
isPublished: false,
|
||||||
|
cartEvent: { connect: { id: lastShift.cartEventId } }
|
||||||
|
}).then((response) => {
|
||||||
|
console.log("New shift created:", response.data);
|
||||||
|
// setShifts([...shifts, response.data]);
|
||||||
|
handleCalDateChange(value);
|
||||||
|
}
|
||||||
|
).catch((error) => {
|
||||||
|
console.error("Error creating new shift:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -621,22 +654,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* <button className={`button m-2 bg-blue-800 ${isOperationInProgress ? 'disabled' : ''}`} onClick={importShifts}>
|
|
||||||
{isOperationInProgress ? <div className="spinner"></div> : 'Import shifts (and missing Publishers) from WORD'}
|
|
||||||
</button>
|
|
||||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts()}>Generate empty shifts</button>
|
|
||||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true)}>Copy last month shifts</button>
|
|
||||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true, true)}>Generate Auto shifts</button>
|
|
||||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(false, true, value)}>Generate Auto shifts DAY</button>
|
|
||||||
// <button className="button m-2" onClick={fetchShifts}>Fetch shifts</button>
|
|
||||||
// <button className="button m-2" onClick={sendMails}>Send mails</button>
|
|
||||||
// <button className="button m-2" onClick={generateXLS}>Generate XLSX</button>
|
|
||||||
// <button className="button m-2" onClick={async () => {
|
|
||||||
// await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}`);
|
|
||||||
// }
|
|
||||||
// }>Delete shifts (selected date's month)</button>
|
|
||||||
// <button className="button m-2" onClick={generateMonthlyStatistics}>Generate statistics</button>
|
|
||||||
*/}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* progress bar holder */}
|
{/* progress bar holder */}
|
||||||
@ -664,9 +681,10 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
|
|
||||||
<h2 className="text-lg font-semibold mb-4">Достъпни за този ден: <span className="text-blue-600">{availablePubs.length}</span></h2>
|
<h2 className="text-lg font-semibold mb-4">Достъпни за този ден: <span className="text-blue-600">{availablePubs.length}</span></h2>
|
||||||
<label className="toggle pb-3">
|
<label className="toggle pb-3">
|
||||||
<input type="checkbox" className="toggle-checkbox" onChange={handleCheckboxChange} />
|
<input type="checkbox" className="toggle-checkbox" id="filterShowWithoutAssignments" onChange={handleCheckboxChange} />
|
||||||
<span className="toggle-slider m-1">без назначения за месеца</span>
|
<span className="toggle-slider m-1">без назначения за месеца</span>
|
||||||
|
<input type="checkbox" className="toggle-checkbox" id="filterIncludeOldAvailabilities" onChange={handleCheckboxChange} />
|
||||||
|
<span className="toggle-slider m-1">със стари предпочитания</span>
|
||||||
</label>
|
</label>
|
||||||
<ul className="w-full max-w-md">
|
<ul className="w-full max-w-md">
|
||||||
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
|
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
|
||||||
@ -736,6 +754,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
allPublishersInfo={availablePubs} />
|
allPublishersInfo={availablePubs} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
onClick={handleCreateNewShift}
|
||||||
|
>
|
||||||
|
Добави нова смяна
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -891,6 +915,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
|
|
||||||
import axiosServer from '../../../src/axiosServer';
|
import axiosServer from '../../../src/axiosServer';
|
||||||
import { start } from 'repl';
|
import { start } from 'repl';
|
||||||
|
import { filter } from 'jszip';
|
||||||
export const getServerSideProps = async (context) => {
|
export const getServerSideProps = async (context) => {
|
||||||
const axios = await axiosServer(context);
|
const axios = await axiosServer(context);
|
||||||
// const baseUrl = common.getBaseUrl();
|
// const baseUrl = common.getBaseUrl();
|
||||||
@ -905,13 +930,6 @@ export const getServerSideProps = async (context) => {
|
|||||||
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
|
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
|
||||||
|
|
||||||
const prismaClient = common.getPrismaClient();
|
const prismaClient = common.getPrismaClient();
|
||||||
// let events = await prismaClient.cartEvent.findMany({ where: { isActive: true } });
|
|
||||||
// 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);
|
//const { data: shifts } = await axios.get(url);
|
||||||
|
|
||||||
|
@ -22,7 +22,8 @@ const SchedulePage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchHtmlContent = async () => {
|
const fetchHtmlContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get('/api/schedule?year=2024&month=1', { responseType: 'text' });
|
// const response = await axiosInstance.get('/api/schedule?year=2024&month=1', { responseType: 'text' });
|
||||||
|
const response = await axiosInstance.get('/api/schedule', { responseType: 'text' });
|
||||||
setHtmlContent(response.data); // Set the fetched HTML content in state
|
setHtmlContent(response.data); // Set the fetched HTML content in state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch schedule:", error);
|
console.error("Failed to fetch schedule:", error);
|
||||||
|
@ -51,7 +51,8 @@ export default function ImportPage() {
|
|||||||
desiredShiftsIndex: -1,
|
desiredShiftsIndex: -1,
|
||||||
dataStartIndex: -1,
|
dataStartIndex: -1,
|
||||||
isActiveIndex: -1,
|
isActiveIndex: -1,
|
||||||
pubTypeIndex: -1
|
pubTypeIndex: -1,
|
||||||
|
gender: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFile = (e) => {
|
const handleFile = (e) => {
|
||||||
@ -111,6 +112,7 @@ export default function ImportPage() {
|
|||||||
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
|
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
|
||||||
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
|
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
|
||||||
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
|
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
|
||||||
|
headerRef.current.gender = header.indexOf("Пол");
|
||||||
|
|
||||||
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
|
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
|
||||||
let date;
|
let date;
|
||||||
@ -147,12 +149,16 @@ export default function ImportPage() {
|
|||||||
|
|
||||||
let isOld = false;
|
let isOld = false;
|
||||||
const row = rawData[i];
|
const row = rawData[i];
|
||||||
let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = true, publisherType = PublisherType.Publisher;
|
let email, phone, names, dateOfInput, oldAvDeleted = false,
|
||||||
//const date = new Date(row[0]).toISOS{tring().slice(0, 10);
|
isTrained = false, desiredShiftsPerMonth = 4, isActive = true,
|
||||||
|
publisherType = PublisherType.Publisher,
|
||||||
|
isMale = 0
|
||||||
|
;
|
||||||
|
//ToDo: structure all vars above as single object:
|
||||||
|
|
||||||
if (mode.mainMode == MODE_PUBLISHERS1) {
|
if (mode.mainMode == MODE_PUBLISHERS1) {
|
||||||
|
|
||||||
email = row[headerRef.current.emailIndex];
|
email = row[headerRef.current.emailIndex];
|
||||||
|
|
||||||
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
|
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
|
||||||
// Remove any non-digit characters, except for the leading +
|
// Remove any non-digit characters, except for the leading +
|
||||||
//phone = phone.replace(/(?!^\+)\D/g, '');
|
//phone = phone.replace(/(?!^\+)\D/g, '');
|
||||||
@ -165,11 +171,11 @@ export default function ImportPage() {
|
|||||||
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
|
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
|
||||||
dateOfInput = importDate.value || new Date().toISOString();
|
dateOfInput = importDate.value || new Date().toISOString();
|
||||||
// not empty == true
|
// not empty == true
|
||||||
|
|
||||||
isTrained = row[headerRef.current.isTrainedIndex] !== '';
|
isTrained = row[headerRef.current.isTrainedIndex] !== '';
|
||||||
isActive = row[headerRef.current.isActiveIndex] == '';
|
isActive = row[headerRef.current.isActiveIndex] == '';
|
||||||
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
|
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
|
||||||
publisherType = row[headerRef.current.pubTypeIndex];
|
publisherType = row[headerRef.current.pubTypeIndex];
|
||||||
|
isMale = row[headerRef.current.gender].trim().toLowerCase() === 'брат';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dateOfInput = common.excelSerialDateToDate(row[0]);
|
dateOfInput = common.excelSerialDateToDate(row[0]);
|
||||||
@ -183,7 +189,7 @@ export default function ImportPage() {
|
|||||||
let day = new Date();
|
let day = new Date();
|
||||||
day.setDate(1); // Set to the first day of the month to avoid overflow
|
day.setDate(1); // Set to the first day of the month to avoid overflow
|
||||||
if (importDate && importDate.value) {
|
if (importDate && importDate.value) {
|
||||||
let monthOfIportInfo = common.getMonthInfo(importDate.value);
|
let monthOfIportInfo = common.getMonthInfo(new Date(importDate.value));
|
||||||
day = monthOfIportInfo.firstMonday;
|
day = monthOfIportInfo.firstMonday;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +207,7 @@ export default function ImportPage() {
|
|||||||
let personNames = names.join(' ');
|
let personNames = names.join(' ');
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const select = "&select=id,firstName,lastName,email,phone,isTrained,desiredShiftsPerMonth,isActive,type,availabilities";
|
const select = "&select=id,firstName,lastName,email,phone,isTrained,desiredShiftsPerMonth,isActive,isMale,type,availabilities";
|
||||||
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
|
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
|
||||||
let existingPublisher = responseByName.data[0];
|
let existingPublisher = responseByName.data[0];
|
||||||
if (!existingPublisher) {
|
if (!existingPublisher) {
|
||||||
@ -244,11 +250,8 @@ export default function ImportPage() {
|
|||||||
} else {
|
} else {
|
||||||
data[i - mode.headerRow][4] = "existing";
|
data[i - mode.headerRow][4] = "existing";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log existing publisher
|
|
||||||
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
|
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
|
||||||
|
|
||||||
|
|
||||||
// Check for other updates
|
// Check for other updates
|
||||||
const fieldsToUpdate = [
|
const fieldsToUpdate = [
|
||||||
{ key: 'email', value: email },
|
{ key: 'email', value: email },
|
||||||
@ -256,6 +259,7 @@ export default function ImportPage() {
|
|||||||
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
||||||
{ key: 'isTrained', value: isTrained },
|
{ key: 'isTrained', value: isTrained },
|
||||||
{ key: 'isActive', value: isActive },
|
{ key: 'isActive', value: isActive },
|
||||||
|
{ key: "isMale", value: isMale },
|
||||||
{ key: 'type', value: publisherType, parse: common.getPubTypeEnum }
|
{ key: 'type', value: publisherType, parse: common.getPubTypeEnum }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -277,6 +281,7 @@ export default function ImportPage() {
|
|||||||
data[i - mode.headerRow][4] = fieldsToUpdateString.substring(0, fieldsToUpdateString.length - 2)
|
data[i - mode.headerRow][4] = fieldsToUpdateString.substring(0, fieldsToUpdateString.length - 2)
|
||||||
+ " updated";
|
+ " updated";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
data[i - mode.headerRow][4] = "error updating!";
|
||||||
console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error);
|
console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,6 +314,7 @@ export default function ImportPage() {
|
|||||||
firstName: firstname,
|
firstName: firstname,
|
||||||
lastName: names[names.length - 1],
|
lastName: names[names.length - 1],
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
|
isMale: isMale,
|
||||||
isTrained,
|
isTrained,
|
||||||
desiredShiftsPerMonth
|
desiredShiftsPerMonth
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ import common from '../../../src/helpers/common';
|
|||||||
import Modal from 'components/Modal';
|
import Modal from 'components/Modal';
|
||||||
import ConfirmationModal from 'components/ConfirmationModal';
|
import ConfirmationModal from 'components/ConfirmationModal';
|
||||||
import PublisherSearchBox from '../../../components/publisher/PublisherSearchBox'; // Update the path
|
import PublisherSearchBox from '../../../components/publisher/PublisherSearchBox'; // Update the path
|
||||||
|
import SearchReplacement from '../../../components/publisher/SearchReplacement'; // Update the path
|
||||||
|
|
||||||
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../../src/helpers/const"
|
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../../src/helpers/const"
|
||||||
import { useSession, getSession } from 'next-auth/react';
|
import { useSession, getSession } from 'next-auth/react';
|
||||||
@ -52,27 +53,12 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchReplacement = (assignmentId) => {
|
|
||||||
axiosInstance.post('/api/email?action=sendCoverMeRequestByEmail', {
|
|
||||||
assignmentId: assignmentId,
|
|
||||||
}).then(response => {
|
|
||||||
console.log("response", response);
|
|
||||||
//toast success and confirm the change
|
|
||||||
toast.success("Заявката за заместник е изпратена!", {
|
|
||||||
onClose: () => {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
console.log("error", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
||||||
<div className="container mx-auto p-4">
|
<div className="container ">
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{assignments && assignments.map((assignment) => (
|
{assignments && assignments.map((assignment) => (
|
||||||
<div key={assignment.dateStr + assignments.indexOf(assignment)} className="bg-white shadow overflow-hidden rounded-lg">
|
<div key={assignment.dateStr + assignments.indexOf(assignment)} className="bg-white shadow overflow-hidden rounded-lg">
|
||||||
@ -81,18 +67,18 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200">
|
<div className="border-t border-gray-200">
|
||||||
<dl>
|
<dl>
|
||||||
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
<div className="bg-gray-50 px-4 py-5 grid grid-cols-1 sm:grid-cols-3 gap-4 xs:gap-1 px-6 xs:py-1">
|
||||||
<dt className="text-sm font-medium text-gray-500">Час</dt>
|
<dt className="text-sm font-medium text-gray-500">Час</dt>
|
||||||
<dd className="mt-1 text text-gray-900 sm:mt-0 sm:col-span-2">
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
{GetTimeFormat(assignment.shift.startTime)} - {GetTimeFormat(assignment.shift.endTime)}
|
{GetTimeFormat(assignment.shift.startTime)} - {GetTimeFormat(assignment.shift.endTime)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
<div className="bg-gray-50 px-4 py-5 grid grid-cols-1 sm:grid-cols-3 gap-4 xs:gap-1 px-6 xs:py-1">
|
||||||
<dt className="text-sm font-medium text-gray-500">Смяна</dt>
|
<dt className="text-sm font-medium text-gray-500">Смяна</dt>
|
||||||
<dd className="mt-1 text text-gray-900 sm:mt-0 sm:col-span-2">
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
{assignment.shift.assignments.map((a, index) => {
|
{assignment.shift.assignments.map((a, index) => {
|
||||||
return (
|
return (
|
||||||
<span key={index} className="inline-flex items-center mr-1 px-2 py-0.5 border border-gray-300 rounded-full text-sm font-medium bg-gray-100">
|
<span key={index} className="inline-flex items-center mr-1 px-2 py-0.5 my-0.5 border border-gray-300 rounded-full text-sm font-medium bg-gray-100">
|
||||||
{a.publisher.firstName} {a.publisher.lastName}
|
{a.publisher.firstName} {a.publisher.lastName}
|
||||||
{a.isWithTransport && <LocalShippingIcon style={{ marginLeft: '4px' }} />}
|
{a.isWithTransport && <LocalShippingIcon style={{ marginLeft: '4px' }} />}
|
||||||
</span>
|
</span>
|
||||||
@ -101,7 +87,7 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
<div className="bg-gray-50 px-4 py-5 grid grid-cols-1 sm:grid-cols-3 gap-4 xs:gap-1 px-6 xs:py-1">
|
||||||
<dt className="text-sm font-medium text-gray-500">Действия</dt>
|
<dt className="text-sm font-medium text-gray-500">Действия</dt>
|
||||||
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
<button
|
<button
|
||||||
@ -121,12 +107,8 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
>
|
>
|
||||||
Избери Заместник
|
Избери Заместник
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="mr-2 mb-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
<SearchReplacement shiftId={assignment.shift.id} assignmentId={assignment.id} />
|
||||||
onClick={() => searchReplacement(assignment.id)}
|
|
||||||
>
|
|
||||||
Търси заместник
|
|
||||||
</button>
|
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
@ -223,7 +205,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignments = publisher?.assignments || [];
|
const assignments = publisher?.assignments.filter(assignment => assignment.shift.startTime >= lastSunday) || [];
|
||||||
|
|
||||||
const transformedAssignments = assignments?.map(assignment => {
|
const transformedAssignments = assignments?.map(assignment => {
|
||||||
if (assignment.shift && assignment.shift.startTime) {
|
if (assignment.shift && assignment.shift.startTime) {
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Layout from "../../../components/layout";
|
import Layout from "../../../components/layout";
|
||||||
import ProtectedRoute from '../../../components/protectedRoute';
|
import ProtectedRoute from '../../../components/protectedRoute';
|
||||||
import { UserRole } from '@prisma/client';
|
import { Prisma, UserRole } from '@prisma/client';
|
||||||
import axiosServer from '../../../src/axiosServer';
|
import axiosServer from '../../../src/axiosServer';
|
||||||
import common from '../../../src/helpers/common';
|
import common from '../../../src/helpers/common';
|
||||||
import { filterPublishers, /* other functions */ } from '../../api/index';
|
// import { filterPublishers, /* other functions */ } from '../../api/index';
|
||||||
|
import data from '../../../src/helpers/data';
|
||||||
|
|
||||||
function ContactsPage({ publishers }) {
|
// const data = require('../../src/helpers/data');
|
||||||
|
|
||||||
|
function ContactsPage({ publishers, allPublishers }) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const filteredPublishers = publishers.filter((publisher) =>
|
const filteredPublishers = allPublishers.filter((publisher) =>
|
||||||
publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
publisher.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
publisher.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
publisher.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
publisher.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
@ -21,7 +24,7 @@ function ContactsPage({ publishers }) {
|
|||||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="text-xl font-semibold mb-4">Статистика </h1>
|
<h1 className="text-xl font-semibold mb-4">Статистика </h1>
|
||||||
<h5 className="text-lg font-semibold mb-4">{publishers.length} участника с предпочитания</h5>
|
<h5 className="text-lg font-semibold mb-4">{publishers.length} участника с предпочитания за месеца (от {allPublishers.length} )</h5>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Търси по име, имейл или телефон..."
|
placeholder="Търси по име, имейл или телефон..."
|
||||||
@ -39,26 +42,55 @@ function ContactsPage({ publishers }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredPublishers.map((pub) => (
|
{filteredPublishers.map((allPub) => {
|
||||||
<tr key={pub.id}>
|
// Find the publisher in the publishers collection to access statistics
|
||||||
<td className="border-b p-4 pl-8" title={pub.lastUpdate}>{pub.firstName} {pub.lastName}</td>
|
const pub = publishers.find(publisher => publisher.id === allPub.id);
|
||||||
<td className="border-b p-4">
|
|
||||||
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
|
return (
|
||||||
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
<tr key={allPub.id}>
|
||||||
</span>
|
<td className="border-b p-4 pl-8" title={allPub.lastUpdate}>{allPub.firstName} {allPub.lastName}</td>
|
||||||
</td>
|
{/* Display statistics if publisher is found */}
|
||||||
<td className="border-b p-4">
|
{pub ? (
|
||||||
<div className="flex items-center justify-between">
|
<>
|
||||||
<div className="flex items-center">
|
<td className="border-b p-4">
|
||||||
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
|
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`}>
|
||||||
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
|
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
||||||
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
</span>
|
||||||
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
</td>
|
||||||
</div>
|
<td className="border-b p-4">
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
</td>
|
<div className="flex items-center">
|
||||||
</tr>
|
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
|
||||||
))}
|
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
|
||||||
|
<span title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
||||||
|
<button title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td className="border-b p-4">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td className="border-b p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="badge py-1 px-2 rounded-md text-xs bg-gray-300 text-gray-500" title="участия този месец">
|
||||||
|
{allPub.currentMonthAssignments || 0}
|
||||||
|
</span>
|
||||||
|
<span className="badge py-1 px-2 rounded-md text-xs bg-gray-300 text-gray-500" title="участия миналия месец">
|
||||||
|
{allPub.previousMonthAssignments || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="border-b p-4"></td> {/* Empty cell for alignment */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -71,10 +103,29 @@ function ContactsPage({ publishers }) {
|
|||||||
|
|
||||||
export default ContactsPage;
|
export default ContactsPage;
|
||||||
|
|
||||||
|
|
||||||
|
// Helper functions ToDo: move them to common and replace all implementations with the common ones
|
||||||
|
function countAssignments(assignments, startTime, endTime) {
|
||||||
|
return assignments.filter(assignment =>
|
||||||
|
assignment.shift.startTime >= startTime && assignment.shift.startTime <= endTime
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertShiftDates(assignments) {
|
||||||
|
assignments.forEach(assignment => {
|
||||||
|
if (assignment.shift && assignment.shift.startTime) {
|
||||||
|
assignment.shift.startTime = new Date(assignment.shift.startTime).toISOString();
|
||||||
|
assignment.shift.endTime = new Date(assignment.shift.endTime).toISOString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context) => {
|
export const getServerSideProps = async (context) => {
|
||||||
|
|
||||||
|
const prisma = common.getPrismaClient();
|
||||||
const dateStr = new Date().toISOString().split('T')[0];
|
const dateStr = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
let publishers = await filterPublishers('id,firstName,lastName,email,isActive,desiredShiftsPerMonth', "", new Date(), true, true, false);
|
let publishers = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth', dateStr, false, true, true);
|
||||||
|
|
||||||
// const axios = await axiosServer(context);
|
// const axios = await axiosServer(context);
|
||||||
// const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
// const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
||||||
@ -115,9 +166,67 @@ export const getServerSideProps = async (context) => {
|
|||||||
//remove publishers without availabilities
|
//remove publishers without availabilities
|
||||||
publishers = publishers.filter(publisher => publisher.availabilities.length > 0);
|
publishers = publishers.filter(publisher => publisher.availabilities.length > 0);
|
||||||
|
|
||||||
|
let allPublishers = await prisma.publisher.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
phone: true,
|
||||||
|
isActive: true,
|
||||||
|
desiredShiftsPerMonth: true,
|
||||||
|
assignments: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
shift: {
|
||||||
|
select: {
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let monthInfo,
|
||||||
|
currentMonthStart, currentMonthEnd,
|
||||||
|
previousMonthStart, previousMonthEnd;
|
||||||
|
|
||||||
|
monthInfo = common.getMonthDatesInfo(new Date());
|
||||||
|
currentMonthStart = monthInfo.firstMonday;
|
||||||
|
currentMonthEnd = monthInfo.lastSunday;
|
||||||
|
let prevMnt = new Date();
|
||||||
|
prevMnt.setMonth(prevMnt.getMonth() - 1);
|
||||||
|
monthInfo = common.getMonthDatesInfo(prevMnt);
|
||||||
|
previousMonthStart = monthInfo.firstMonday;
|
||||||
|
previousMonthEnd = monthInfo.lastSunday;
|
||||||
|
|
||||||
|
|
||||||
|
allPublishers.forEach(publisher => {
|
||||||
|
// Use helper functions to calculate and assign assignment counts
|
||||||
|
publisher.currentMonthAssignments = countAssignments(publisher.assignments, currentMonthStart, currentMonthEnd);
|
||||||
|
publisher.previousMonthAssignments = countAssignments(publisher.assignments, previousMonthStart, previousMonthEnd);
|
||||||
|
|
||||||
|
// Convert date formats within the same iteration
|
||||||
|
convertShiftDates(publisher.assignments);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optionally, if you need a transformed list or additional properties, map the publishers
|
||||||
|
allPublishers = allPublishers.map(publisher => ({
|
||||||
|
...publisher,
|
||||||
|
// Potentially add more computed properties or transformations here if needed
|
||||||
|
}));
|
||||||
|
allPublishers.sort((a, b) => a.firstName.localeCompare(b.firstName) || a.lastName.localeCompare(b.lastName));
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
publishers,
|
publishers,
|
||||||
|
allPublishers,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9,16 +9,35 @@ import { useSession } from "next-auth/react"
|
|||||||
import common from '../../../src/helpers/common';
|
import common from '../../../src/helpers/common';
|
||||||
import Layout from "../../../components/layout";
|
import Layout from "../../../components/layout";
|
||||||
import ProtectedRoute from '../../../components/protectedRoute';
|
import ProtectedRoute from '../../../components/protectedRoute';
|
||||||
import { Location, UserRole } from "@prisma/client";
|
import { Location, UserRole, ReportType } from "@prisma/client";
|
||||||
|
|
||||||
|
|
||||||
export default function Reports() {
|
export default function Reports() {
|
||||||
const [reports, setReports] = useState([]);
|
const [reports, setReports] = useState([]);
|
||||||
|
const [filteredReports, setFilteredReports] = useState([]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const [filterType, setFilterType] = useState('ServiceReport');
|
||||||
|
|
||||||
|
const handleFilterChange = (event) => {
|
||||||
|
setFilterType(event.target.value);
|
||||||
|
console.log("event filter set to:" + event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isFeedbackType = type => [
|
||||||
|
'Feedback_Problem',
|
||||||
|
'Feedback_Suggestion',
|
||||||
|
'Feedback'
|
||||||
|
].includes(type);
|
||||||
|
|
||||||
|
setFilteredReports(reports.filter(report =>
|
||||||
|
filterType === 'Feedback' ? isFeedbackType(report.type) : report.type === filterType
|
||||||
|
));
|
||||||
|
}, [reports, filterType]);
|
||||||
|
|
||||||
const deleteReport = (id) => {
|
const deleteReport = (id) => {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
@ -66,7 +85,7 @@ export default function Reports() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
||||||
|
|
||||||
<div className="h-5/6 grid place-items-center">
|
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
||||||
<div className="flex flex-col w-full px-4">
|
<div className="flex flex-col w-full px-4">
|
||||||
<h1 className="text-2xl font-bold text-center">Отчети</h1>
|
<h1 className="text-2xl font-bold text-center">Отчети</h1>
|
||||||
<Link href="/cart/reports/report">
|
<Link href="/cart/reports/report">
|
||||||
@ -74,18 +93,21 @@ export default function Reports() {
|
|||||||
Добави нов отчет
|
Добави нов отчет
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<label className="mr-4">
|
<div className="flex gap-2 mb-4">
|
||||||
<input type="radio" name="reportType" value="ServiceReport" defaultChecked />
|
|
||||||
Отчети
|
<label className={`cursor-pointer px-4 py-2 rounded-full ${filterType === 'ServiceReport' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||||
</label>
|
<input type="radio" name="reportType" value="ServiceReport" checked={filterType === 'ServiceReport'} onChange={handleFilterChange} className="sr-only" />
|
||||||
<label className="mr-4">
|
Отчети
|
||||||
<input type="radio" name="reportType" value="Experience" />
|
</label>
|
||||||
Случка
|
<label className={`cursor-pointer px-4 py-2 rounded-full ${filterType === 'Experience' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||||
</label>
|
<input type="radio" name="reportType" value="Experience" checked={filterType === 'Experience'} onChange={handleFilterChange} className="sr-only" />
|
||||||
<label className="mr-4">
|
Случка
|
||||||
<input type="radio" name="reportType" value="Feedback" />
|
</label>
|
||||||
Отзиви
|
<label className={`cursor-pointer px-4 py-2 rounded-full ${filterType === 'Feedback' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||||
</label>
|
<input type="radio" name="reportType" value="Feedback" checked={filterType === 'Feedback'} onChange={handleFilterChange} className="sr-only" />
|
||||||
|
Отзиви
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div className="mt-4 w-full overflow-x-auto">
|
<div className="mt-4 w-full overflow-x-auto">
|
||||||
<table className="w-full table-auto">
|
<table className="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
@ -98,13 +120,13 @@ export default function Reports() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{reports.map((report) => (
|
{filteredReports.map((report) => (
|
||||||
<tr key={report.id}>
|
<tr key={report.id}>
|
||||||
<td className="border px-2 py-2">{report.publisher.firstName + " " + report.publisher.lastName}</td>
|
<td className="border px-2 py-2">{report.publisher.firstName + " " + report.publisher.lastName}</td>
|
||||||
<td className="border px-2 py-2">{common.getDateFormated(new Date(report.date))}</td>
|
<td className="border px-2 py-2">{common.getDateFormated(new Date(report.date))}</td>
|
||||||
<td className="border px-2 py-2">{report.location?.name}</td>
|
<td className="border px-2 py-2">{report.location?.name}</td>
|
||||||
<td className="border px-2 py-2">
|
<td className="border px-2 py-2">
|
||||||
{(report.experienceInfo === null || report.experienceInfo === "")
|
{(report.type === ReportType.ServiceReport)
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<div><strong>Отчет</strong></div>
|
<div><strong>Отчет</strong></div>
|
||||||
@ -113,9 +135,16 @@ export default function Reports() {
|
|||||||
Клипове: {report.videoCount} <br />
|
Клипове: {report.videoCount} <br />
|
||||||
Адреси / Телефони: {report.returnVisitInfoCount} <br />
|
Адреси / Телефони: {report.returnVisitInfoCount} <br />
|
||||||
</>
|
</>
|
||||||
) : (report.placementCount > 0) ? (
|
) : (report.type === ReportType.Feedback || report.type === ReportType.Feedback_Problem || report.type === ReportType.Feedback_Suggestion) ? (
|
||||||
<>
|
<>
|
||||||
<div><strong>Отзив</strong></div>
|
<div>
|
||||||
|
<strong style={{ fontWeight: 'bold' }}>Отзив</strong>
|
||||||
|
{report.type === "Feedback_Problem" ?
|
||||||
|
<span style={{ color: 'red', fontWeight: 'bold' }}> - Проблем</span> :
|
||||||
|
report.type === "Feedback_Suggestion" ?
|
||||||
|
<span style={{ color: 'blue' }}> - Предложение</span> :
|
||||||
|
""}
|
||||||
|
</div>
|
||||||
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
|
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -88,6 +88,7 @@ async function getAvailabilities(userId) {
|
|||||||
name: true,
|
name: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isFromPreviousAssignment: true,
|
isFromPreviousAssignment: true,
|
||||||
|
isFromPreviousMonth: true,
|
||||||
dayofweek: true,
|
dayofweek: true,
|
||||||
dayOfMonth: true,
|
dayOfMonth: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
|
@ -3,35 +3,61 @@ import Layout from "../components/layout";
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { url } from 'inspector';
|
import { url } from 'inspector';
|
||||||
|
import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute";
|
||||||
|
import axiosInstance from '../src/axiosSecure';
|
||||||
|
|
||||||
|
|
||||||
const PDFViewerPage = ({ pdfFiles }) => {
|
const PDFViewerPage = ({ pdfFiles }) => {
|
||||||
|
const [files, setFiles] = useState(pdfFiles);
|
||||||
|
|
||||||
|
const handleFileDelete = async (fileName) => {
|
||||||
|
const subfolder = 'permits'; // Change this as needed based on your subfolder structure
|
||||||
|
try {
|
||||||
|
await axiosInstance.delete(`/api/content/${subfolder}?file=${fileName}`);
|
||||||
|
setFiles(files.filter(file => file.name !== fileName));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting file:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const subfolder = 'permits'; // Change this as needed based on your subfolder structure
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(`/api/content/${subfolder}`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setFiles([...files, response.data]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading file:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
|
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
|
||||||
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
<ProtectedRoute>
|
||||||
{/* <p className="p-1">
|
<input type="file" onChange={handleFileUpload} className="mb-4" />
|
||||||
{pdfFiles.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<p className="p-2">
|
<div key={file.name} className="py-2">
|
||||||
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||||
Свали: {file.name}
|
{file.name}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">
|
||||||
))}
|
изтрий
|
||||||
</p> */}
|
</button>
|
||||||
{pdfFiles.map((file, index) => (
|
</div>
|
||||||
|
))}
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
// <React.Fragment key={file.name}>
|
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
||||||
// {index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
|
{pdfFiles.map((file, index) => (
|
||||||
// <a
|
|
||||||
// href={file.url}
|
|
||||||
// target="_blank"
|
|
||||||
// className={`text-lg py-2 px-4 bg-gray-200 text-gray-800 hover:bg-blue-500 hover:text-white ${index === 0 ? 'rounded-l-full' : index === pdfFiles.length - 1 ? 'rounded-r-full' : ''}`}
|
|
||||||
// >
|
|
||||||
// {file.name}
|
|
||||||
// </a>
|
|
||||||
// </React.Fragment>
|
|
||||||
<> <p className="pt-2">
|
<> <p className="pt-2">
|
||||||
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||||
Свали: {file.name}
|
Свали: {file.name}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `EventLog` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`date` DATETIME(3) NOT NULL,
|
||||||
|
`publisherId` VARCHAR(191) NULL,
|
||||||
|
`shiftId` INTEGER NULL,
|
||||||
|
`content` VARCHAR(5000) NOT NULL,
|
||||||
|
`type` ENUM('AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail') NOT NULL,
|
||||||
|
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `EventLog`
|
||||||
|
ADD CONSTRAINT `EventLog_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `EventLog`
|
||||||
|
ADD CONSTRAINT `EventLog_shiftId_fkey` FOREIGN KEY (`shiftId`) REFERENCES `Shift` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -121,6 +121,7 @@ model Publisher {
|
|||||||
comments String?
|
comments String?
|
||||||
reports Report[]
|
reports Report[]
|
||||||
Message Message[]
|
Message Message[]
|
||||||
|
EventLog EventLog[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Availability {
|
model Availability {
|
||||||
@ -179,6 +180,7 @@ model Shift {
|
|||||||
reportId Int? @unique
|
reportId Int? @unique
|
||||||
Report Report? @relation(fields: [reportId], references: [id])
|
Report Report? @relation(fields: [reportId], references: [id])
|
||||||
isPublished Boolean @default(false) //NEW v1.0.1
|
isPublished Boolean @default(false) //NEW v1.0.1
|
||||||
|
EventLog EventLog[]
|
||||||
|
|
||||||
@@map("Shift")
|
@@map("Shift")
|
||||||
}
|
}
|
||||||
@ -256,6 +258,23 @@ model Message {
|
|||||||
type MessageType @default(Email)
|
type MessageType @default(Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EventLogType {
|
||||||
|
AssignmentReplacementRequested
|
||||||
|
AssignmentReplacementAccepted
|
||||||
|
SentEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
model EventLog {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
date DateTime
|
||||||
|
publisherId String?
|
||||||
|
publisher Publisher? @relation(fields: [publisherId], references: [id])
|
||||||
|
shiftId Int?
|
||||||
|
shift Shift? @relation(fields: [shiftId], references: [id])
|
||||||
|
content String @db.VarChar(5000)
|
||||||
|
type EventLogType
|
||||||
|
}
|
||||||
|
|
||||||
//user auth and session management
|
//user auth and session management
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
BIN
public/content/permits/Разрешително за Април - 24г..pdf
Normal file
BIN
public/content/permits/Разрешително за Април - 24г..pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -20,7 +20,7 @@
|
|||||||
"dir": "auto",
|
"dir": "auto",
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
"name": "Специално Свидетелстване София",
|
"name": "Специално Свидетелстване София",
|
||||||
"short_name": "ССС",
|
"short_name": "ССОМ",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"scope": "/cart"
|
"scope": "/cart"
|
||||||
}
|
}
|
32
server.js
32
server.js
@ -40,6 +40,20 @@ console.log("process.env.PORT = ", process.env.PORT);
|
|||||||
console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT);
|
console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT);
|
||||||
console.log("process.env.DATABASE_URL = ", process.env.DATABASE_URL);
|
console.log("process.env.DATABASE_URL = ", process.env.DATABASE_URL);
|
||||||
console.log("process.env.DATABASE = ", process.env.DATABASE);
|
console.log("process.env.DATABASE = ", process.env.DATABASE);
|
||||||
|
console.log("process.env.APPLE_APP_ID = ", process.env.APPLE_APP_ID);
|
||||||
|
|
||||||
|
|
||||||
|
// update GIT_COMMIT_ID
|
||||||
|
const { exec } = require("child_process");
|
||||||
|
exec("git rev-parse HEAD", (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(`exec error: ${error}`);
|
||||||
|
process.env.GIT_COMMIT_ID = "unknown";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.env.GIT_COMMIT_ID = stdout.trim();
|
||||||
|
console.log("GIT_COMMIT_ID = ", process.env.GIT_COMMIT_ID);
|
||||||
|
});
|
||||||
|
|
||||||
//require('module-alias/register');
|
//require('module-alias/register');
|
||||||
|
|
||||||
@ -92,29 +106,13 @@ nextApp
|
|||||||
// Add the middleware to set 'x-forwarded-host' header
|
// Add the middleware to set 'x-forwarded-host' header
|
||||||
server.use((req, res, next) => {
|
server.use((req, res, next) => {
|
||||||
req.headers['x-forwarded-host'] = req.headers['x-forwarded-host'] || req.headers.host;
|
req.headers['x-forwarded-host'] = req.headers['x-forwarded-host'] || req.headers.host;
|
||||||
// ---------------
|
|
||||||
// if (!baseUrlGlobal) {
|
|
||||||
// const protocol = req.headers['x-forwarded-proto'] || 'http';
|
|
||||||
// const host = req.headers.host;
|
|
||||||
// const baseUrl = `${protocol}://${host}`;
|
|
||||||
// baseUrlGlobal = baseUrl;
|
|
||||||
// fs.writeFileSync(path.join(__dirname, 'baseUrl.txt'), baseUrlGlobal, 'utf8');
|
|
||||||
// console.log("baseUrlGlobal set to: " + baseUrlGlobal);
|
|
||||||
// }
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico"));
|
server.use("/favicon.ico", express.static("public/favicon.png"));
|
||||||
// server.use("/robots.txt", express.static("styles/favicon_io/robots.txt"));
|
// server.use("/robots.txt", express.static("styles/favicon_io/robots.txt"));
|
||||||
// server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
|
// server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
|
||||||
|
|
||||||
server.get("/last_schedule_json", (req, res) => {
|
server.get("/last_schedule_json", (req, res) => {
|
||||||
// var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
|
|
||||||
// const newData = data.map((item) => {
|
|
||||||
// const names = item.names.filter((name) => {
|
|
||||||
// return !name.startsWith('Прибира количка') && !name.startsWith('Докарва количка');
|
|
||||||
// });
|
|
||||||
// return { ...item, names };
|
|
||||||
// });
|
|
||||||
res.json(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
|
res.json(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,18 +6,11 @@ const levenshtein = require('fastest-levenshtein');
|
|||||||
|
|
||||||
const fs = typeof window === 'undefined' ? require('fs') : undefined;
|
const fs = typeof window === 'undefined' ? require('fs') : undefined;
|
||||||
const path = typeof window === 'undefined' ? require('path') : undefined;
|
const path = typeof window === 'undefined' ? require('path') : undefined;
|
||||||
|
const { PrismaClient, UserRole } = require('@prisma/client');
|
||||||
const { PrismaClient } = require('@prisma/client');
|
|
||||||
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
||||||
|
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
// User and auth functions
|
|
||||||
// import { getSession } from "next-auth/react";
|
|
||||||
// import { UserRole } from "@prisma/client";
|
|
||||||
//convert to es6 import
|
|
||||||
const { getSession } = require("next-auth/react");
|
const { getSession } = require("next-auth/react");
|
||||||
const { UserRole } = require("@prisma/client");
|
|
||||||
// const { set } = require('date-fns');
|
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info', // Set the default log level
|
level: 'info', // Set the default log level
|
||||||
@ -249,6 +242,8 @@ exports.getDateFromWeekNrAndDayOfWeek = function (firstMonday, weekNr, dayOfWeek
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.getMonthDatesInfo = function (date) {
|
exports.getMonthDatesInfo = function (date) {
|
||||||
|
// cast to date if not daate
|
||||||
|
date = new Date(date);
|
||||||
// get first day of the month
|
// get first day of the month
|
||||||
var firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
var firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||||
// get first day of next month
|
// get first day of next month
|
||||||
@ -327,7 +322,23 @@ exports.getWeekNumber = function (date) {
|
|||||||
return Math.ceil((date.getDate() - info.firstMonday.getDate() + 1) / 7);
|
return Math.ceil((date.getDate() - info.firstMonday.getDate() + 1) / 7);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.compareTimes = function (time1, time2) {
|
||||||
|
const time1String = `${getHours(time1)}:${getMinutes(time1)}`;
|
||||||
|
const time2String = `${getHours(time2)}:${getMinutes(time2)}`;
|
||||||
|
return time1String.localeCompare(time2String);
|
||||||
|
};
|
||||||
|
exports.normalizeTime = function (date, baseDate) {
|
||||||
|
// return set(baseDate, {
|
||||||
|
// hours: getHours(date),
|
||||||
|
// minutes: getMinutes(date),
|
||||||
|
// seconds: getSeconds(date),
|
||||||
|
// milliseconds: 0
|
||||||
|
// });
|
||||||
|
//don't use date-fns
|
||||||
|
let newDate = new Date(baseDate);
|
||||||
|
newDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), 0);
|
||||||
|
return newDate;
|
||||||
|
}
|
||||||
|
|
||||||
exports.getTimeRange = function (start, end) {
|
exports.getTimeRange = function (start, end) {
|
||||||
start = new Date(start);
|
start = new Date(start);
|
||||||
@ -346,6 +357,12 @@ exports.getDateFormated = function (date) {
|
|||||||
return `${dayOfWeekName} ${day} ${monthName} ${year} г.`;
|
return `${dayOfWeekName} ${day} ${monthName} ${year} г.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getDateFormatedShort = function (date) {
|
||||||
|
const day = date.getDate();
|
||||||
|
const monthName = exports.getMonthName(date.getMonth());
|
||||||
|
return `${day} ${monthName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.getTimeFomatted = function (date) {
|
exports.getTimeFomatted = function (date) {
|
||||||
return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });//timeZone: 'local'
|
return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });//timeZone: 'local'
|
||||||
@ -735,3 +752,23 @@ exports.getLocalStorage = function (key, defaultValue) {
|
|||||||
exports.root = function (req) {
|
exports.root = function (req) {
|
||||||
return process.env.NEXT_PUBLIC_PUBLIC_URL;
|
return process.env.NEXT_PUBLIC_PUBLIC_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getInitials = function (names) {
|
||||||
|
const parts = names.split(' ');
|
||||||
|
return parts.map(part => part[0] + ".").join('');
|
||||||
|
}
|
||||||
|
// exports.getInitials = function (names) {
|
||||||
|
// const parts = names.split(' '); // Split the full name into parts
|
||||||
|
// if (parts.length === 0) {
|
||||||
|
// return '';
|
||||||
|
// }
|
||||||
|
// // Extract the first two letters of the first name
|
||||||
|
// let initials = parts[0].substring(0, 2) + ".";
|
||||||
|
|
||||||
|
// // If there is a last name, add the first letter of the last name
|
||||||
|
// if (parts.length > 1) {
|
||||||
|
// initials += parts[parts.length - 1][0] + ".";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return initials;
|
||||||
|
// }
|
@ -226,6 +226,273 @@ async function getAvailabilities(userId) {
|
|||||||
return serializableItems;
|
return serializableItems;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true, includeOldAvailabilities = false) {
|
||||||
|
|
||||||
|
filterDate = new Date(filterDate); // Convert to date object if not already
|
||||||
|
|
||||||
|
// Only attempt to split if selectFields is a string; otherwise, use it as it is.
|
||||||
|
selectFields = typeof selectFields === 'string' ? selectFields.split(",") : selectFields;
|
||||||
|
|
||||||
|
let selectBase = selectFields.reduce((acc, curr) => {
|
||||||
|
acc[curr] = true;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
selectBase.assignments = {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
shift: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
shift: {
|
||||||
|
startTime: {
|
||||||
|
gte: filterDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var monthInfo = common.getMonthDatesInfo(filterDate);
|
||||||
|
var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber
|
||||||
|
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate);
|
||||||
|
if (!isExactTime) {
|
||||||
|
filterDate.setHours(0, 0, 0, 0); // Set to midnight
|
||||||
|
}
|
||||||
|
const filterDateEnd = new Date(filterDate);
|
||||||
|
filterDateEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
|
||||||
|
let whereClause = {};
|
||||||
|
//if full day, match by date only
|
||||||
|
if (!isExactTime) { // Check only by date without considering time ( Assignments on specific days without time)
|
||||||
|
whereClause["availabilities"] = {
|
||||||
|
some: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
startTime: { gte: filterDate },
|
||||||
|
endTime: { lte: filterDateEnd },
|
||||||
|
}
|
||||||
|
,
|
||||||
|
// 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, // includes monthly and weekly repeats
|
||||||
|
dayofweek: dayOfWeekEnum,
|
||||||
|
// 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: [
|
||||||
|
// Check if dayOfMonth is set and filterDate is between start and end dates (Assignments on specific days AND time)
|
||||||
|
{
|
||||||
|
// dayOfMonth: filterDate.getDate(),
|
||||||
|
startTime: { lte: filterDate },
|
||||||
|
endTime: { gte: filterDate }
|
||||||
|
},
|
||||||
|
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
||||||
|
{
|
||||||
|
dayOfMonth: null,
|
||||||
|
dayofweek: dayOfWeekEnum,
|
||||||
|
startTime: { gte: filterDate },
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
||||||
|
{ endDate: { gte: filterDate } },
|
||||||
|
{ endDate: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isForTheMonth) {
|
||||||
|
// If no filter date, return all publishers's availabilities for currentMonthStart
|
||||||
|
|
||||||
|
whereClause["availabilities"] = {
|
||||||
|
some: {
|
||||||
|
OR: [
|
||||||
|
// Check if dayOfMonth is not null and startTime is after monthInfo.firstMonday (Assignments on specific days AND time)
|
||||||
|
{
|
||||||
|
dayOfMonth: { not: null },
|
||||||
|
startTime: { gte: monthInfo.firstMonday },
|
||||||
|
endTime: { lte: monthInfo.lastSunday }
|
||||||
|
},
|
||||||
|
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
||||||
|
{
|
||||||
|
dayOfMonth: null,
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
||||||
|
{ endDate: { gte: filterDate } },
|
||||||
|
{ endDate: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`getting publishers for date: ${filterDate}, isExactTime: ${isExactTime}, isForTheMonth: ${isForTheMonth}`);
|
||||||
|
//include availabilities if flag is true
|
||||||
|
const prisma = common.getPrismaClient(); //why we need to get it again?
|
||||||
|
let publishers = await prisma.publisher.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
select: {
|
||||||
|
...selectBase,
|
||||||
|
availabilities: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`);
|
||||||
|
|
||||||
|
// convert matching weekly availabilities to availabilities for the day to make furter processing easier on the client.
|
||||||
|
// we trust that the filtering was OK, so we use the dateFilter as date.
|
||||||
|
publishers.forEach(pub => {
|
||||||
|
pub.availabilities = pub.availabilities.map(avail => {
|
||||||
|
if (avail.dayOfMonth == null) {
|
||||||
|
let newStart = new Date(filterDate);
|
||||||
|
newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
|
||||||
|
let newEnd = new Date(filterDate);
|
||||||
|
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
|
||||||
|
return {
|
||||||
|
...avail,
|
||||||
|
startTime: newStart,
|
||||||
|
endTime: newEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avail;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let currentWeekStart, currentWeekEnd,
|
||||||
|
currentMonthStart, currentMonthEnd,
|
||||||
|
previousMonthStart, previousMonthEnd;
|
||||||
|
|
||||||
|
if (isWithStats) {
|
||||||
|
currentWeekStart = common.getStartOfWeek(filterDate);
|
||||||
|
currentWeekEnd = common.getEndOfWeek(filterDate);
|
||||||
|
currentMonthStart = monthInfo.firstMonday;
|
||||||
|
currentMonthEnd = monthInfo.lastSunday;
|
||||||
|
let prevMnt = new Date(filterDate)
|
||||||
|
prevMnt.setMonth(prevMnt.getMonth() - 1);
|
||||||
|
monthInfo = common.getMonthDatesInfo(prevMnt);
|
||||||
|
previousMonthStart = monthInfo.firstMonday;
|
||||||
|
previousMonthEnd = monthInfo.lastSunday;
|
||||||
|
|
||||||
|
//get if publisher has assignments for current weekday, week, current month, previous month
|
||||||
|
publishers.forEach(pub => {
|
||||||
|
// Filter assignments for current day
|
||||||
|
pub.currentDayAssignments = pub.assignments?.filter(assignment => {
|
||||||
|
return assignment.shift.startTime >= filterDate && assignment.shift.startTime <= filterDateEnd;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
// Filter assignments for current week
|
||||||
|
pub.currentWeekAssignments = pub.assignments?.filter(assignment => {
|
||||||
|
return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
// Filter assignments for current month
|
||||||
|
pub.currentMonthAssignments = pub.assignments?.filter(assignment => {
|
||||||
|
return assignment.shift.startTime >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
// Filter assignments for previous month
|
||||||
|
pub.previousMonthAssignments = pub.assignments?.filter(assignment => {
|
||||||
|
return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd;
|
||||||
|
}).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//get the availabilities for the day. Calcullate:
|
||||||
|
//1. how many days the publisher is available for the current month - only with dayOfMonth
|
||||||
|
//2. how many days the publisher is available without dayOfMonth (previous months count)
|
||||||
|
//3. how many hours in total the publisher is available for the current month
|
||||||
|
publishers.forEach(pub => {
|
||||||
|
if (isWithStats) {
|
||||||
|
pub.currentMonthAvailability = pub.availabilities?.filter(avail => {
|
||||||
|
// return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
|
||||||
|
return avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
|
||||||
|
})
|
||||||
|
pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length || 0;
|
||||||
|
// pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => {
|
||||||
|
// return avail.dayOfMonth == null;
|
||||||
|
// }).length;
|
||||||
|
pub.currentMonthAvailabilityHoursCount = pub.currentMonthAvailability.reduce((acc, curr) => {
|
||||||
|
return acc + (curr.endTime.getTime() - curr.startTime.getTime()) / (1000 * 60 * 60);
|
||||||
|
}, 0);
|
||||||
|
//if pub has up-to-date availabilities (with dayOfMonth) for the current month
|
||||||
|
pub.hasUpToDateAvailabilities = pub.availabilities?.some(avail => {
|
||||||
|
return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//if pub has ever filled the form - if has availabilities which are not from previous assignments
|
||||||
|
pub.hasEverFilledForm = pub.availabilities?.some(avail => {
|
||||||
|
return avail.isFromPreviousAssignments == false;
|
||||||
|
});
|
||||||
|
|
||||||
|
//if pub has availabilities for the current day
|
||||||
|
pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => {
|
||||||
|
return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (isExactTime) {
|
||||||
|
// Post filter for time if dayOfMonth is null as we can't only by time for multiple dates in SQL
|
||||||
|
// Modify the availabilities array of the filtered publishers
|
||||||
|
publishers.forEach(pub => {
|
||||||
|
pub.availabilities = pub.availabilities?.filter(avail => matchesAvailability(avail, filterDate));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return publishers;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function matchesAvailability(avail, filterDate) {
|
||||||
|
// Setting the start and end time of the filterDate
|
||||||
|
filterDate.setHours(0, 0, 0, 0);
|
||||||
|
const filterDateEnd = new Date(filterDate);
|
||||||
|
filterDateEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
// Return true if avail.startTime is between filterDate and filterDateEnd
|
||||||
|
return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
|
||||||
|
}
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@ -255,5 +522,6 @@ module.exports = {
|
|||||||
findPublisher,
|
findPublisher,
|
||||||
findPublisherAvailability,
|
findPublisherAvailability,
|
||||||
runSqlFile,
|
runSqlFile,
|
||||||
getAvailabilities
|
getAvailabilities,
|
||||||
|
filterPublishersNew
|
||||||
};
|
};
|
@ -13,45 +13,32 @@ const Handlebars = require('handlebars');
|
|||||||
|
|
||||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||||
const { env } = require("../../next.config");
|
const { env } = require("../../next.config");
|
||||||
|
const SMTPTransport = require("nodemailer/lib/smtp-transport");
|
||||||
|
|
||||||
// const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
var transporter;
|
||||||
// const SENDER_EMAIL = "sofia@mwitnessing.com";
|
if (process.env.EMAIL_SERVICE.toLowerCase() === "mailtrap") {
|
||||||
// const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL };
|
|
||||||
// const client = new MailtrapClient({ token: TOKEN });
|
|
||||||
|
|
||||||
let mailtrapTestClient = null;
|
transporter = nodemailer.createTransport({
|
||||||
// const mailtrapTestClient = new MailtrapClient({
|
host: process.env.MAILTRAP_HOST || "sandbox.smtp.mailtrap.io",
|
||||||
// username: '8ec69527ff2104',//not working now
|
port: process.env.MAILTRAP_PORT || 2525,
|
||||||
// password: 'c7bc05f171c96c'
|
auth: {
|
||||||
// });
|
user: process.env.MAILTRAP_USER,
|
||||||
|
pass: process.env.MAILTRAP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.EMAIL_SERVICE.toLowerCase() === "gmail") {
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
service: "gmail",
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_GMAIL_USERNAME,
|
||||||
|
pass: process.env.EMAIL_GMAIL_APP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//test
|
|
||||||
var transporter = nodemailer.createTransport({
|
|
||||||
host: process.env.MAILTRAP_HOST || "sandbox.smtp.mailtrap.io",
|
|
||||||
port: 2525,
|
|
||||||
auth: {
|
|
||||||
user: process.env.MAILTRAP_USER,
|
|
||||||
pass: process.env.MAILTRAP_PASS
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// production
|
|
||||||
// var transporter = nodemailer.createTransport({
|
|
||||||
// host: "live.smtp.mailtrap.io",
|
|
||||||
// port: 587,
|
|
||||||
// auth: {
|
|
||||||
// user: "api",
|
|
||||||
// pass: "1cfe82e747b8dc3390ed08bb16e0f48d"
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
var transporterBulk = nodemailer.createTransport({
|
|
||||||
host: "bulk.smtp.mailtrap.io",
|
|
||||||
port: 587,
|
|
||||||
auth: {
|
|
||||||
user: "api",
|
|
||||||
pass: "1cfe82e747b8dc3390ed08bb16e0f48d"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// ------------------ Email sending ------------------
|
// ------------------ Email sending ------------------
|
||||||
var lastResult = null;
|
var lastResult = null;
|
||||||
function setResult(result) {
|
function setResult(result) {
|
||||||
@ -87,8 +74,14 @@ function normalizeEmailAddresses(to) {
|
|||||||
|
|
||||||
|
|
||||||
exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
||||||
let sender = '"Специално Свидетелстване София - тест" <demo@mwitnessing.com>';
|
let sender = process.env.EMAIL_SENDER || '"Специално Свидетелстване София" <sofia@mwitnessing.com>';
|
||||||
const emailAddresses = normalizeEmailAddresses(to)
|
let emailAddresses = normalizeEmailAddresses(to)
|
||||||
|
|
||||||
|
const bypassEmailReccipients = process.env.EMAIL_BYPASS_TO || null;
|
||||||
|
if (bypassEmailReccipients !== null && bypassEmailReccipients.length > 0) {
|
||||||
|
emailAddresses = bypassEmailReccipients;
|
||||||
|
console.log("Emails bypassed. All mails sent to: " + emailAddresses);
|
||||||
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
from: sender,
|
from: sender,
|
||||||
@ -99,22 +92,11 @@ exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
|||||||
attachments
|
attachments
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mailtrapTestClient !== null) {
|
let result = await transporter
|
||||||
// Assuming mailtrapTestClient is correctly set up to send emails
|
.sendMail(message)
|
||||||
await mailtrapTestClient
|
.then(console.log)
|
||||||
.send(message)
|
.catch(console.error);
|
||||||
.then(console.log)
|
return result;
|
||||||
.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 = []) {
|
exports.SendEmailHandlebars = async function (to, templateName, model, attachments = []) {
|
||||||
@ -132,7 +114,7 @@ exports.SendEmailHandlebars = async function (to, templateName, model, attachmen
|
|||||||
const subjectMatch = templateSource.match(/{{!--\s*Subject:\s*(.*?)\s*--}}/);
|
const subjectMatch = templateSource.match(/{{!--\s*Subject:\s*(.*?)\s*--}}/);
|
||||||
const textMatch = templateSource.match(/{{!--\s*Text:\s*([\s\S]*?)\s*--}}/);
|
const textMatch = templateSource.match(/{{!--\s*Text:\s*([\s\S]*?)\s*--}}/);
|
||||||
|
|
||||||
let subject = subjectMatch ? subjectMatch[1].trim() : 'ССС: Известие';
|
let subject = subjectMatch ? subjectMatch[1].trim() : 'ССОМ: Известие';
|
||||||
let textVersion = textMatch ? textMatch[1].trim() : null;
|
let textVersion = textMatch ? textMatch[1].trim() : null;
|
||||||
|
|
||||||
// Remove the subject and text annotations from the template source
|
// Remove the subject and text annotations from the template source
|
||||||
@ -228,7 +210,7 @@ exports.SendEmail_NewShifts = async function (publisher, shifts) {
|
|||||||
// ],
|
// ],
|
||||||
// subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()],
|
// subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()],
|
||||||
// text:
|
// text:
|
||||||
// "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
// "Здравей, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
||||||
// "Ти регистриран да получавате известия за нови смени на количка.\n" +
|
// "Ти регистриран да получавате известия за нови смени на количка.\n" +
|
||||||
// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` +
|
// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` +
|
||||||
// ` ${shftStr} \n\n\n` +
|
// ` ${shftStr} \n\n\n` +
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{{!--Subject: ССС: Нужен е заместник --}}
|
{{!--Subject: ССОМ: Нужен е заместник --}}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>Търси се зместник:
|
<h3>Търси се зместник:
|
||||||
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
|
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
|
||||||
</h3>
|
</h3>
|
||||||
<p>Здравей {{firstName}},</p>
|
<p>Здравей {{firstName}},</p>
|
||||||
<p>{{prefix}} {{user.firstName}} {{user.lastName}} търси заместник.</p>
|
<p>{{user.prefix}} {{user.firstName}} {{user.lastName}} търси заместник.</p>
|
||||||
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
||||||
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||||
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{{!-- Subject: ССС: Промени в твоята смяна --}}
|
{{!-- Subject: ССОМ: Промени в твоята смяна --}}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
|
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
|
||||||
<p>Здравейте {{firstName}}, </p>
|
<p>Здравей {{firstName}}, </p>
|
||||||
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
|
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
|
||||||
<p>Новаия списък с участници за тази смяна е:</p>
|
<p>Новаия списък с участници за тази смяна е:</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{{!-- Subject: ССС: Нужен е заместник--}}
|
{{!-- Subject: ССОМ: Нужен е заместник--}}
|
||||||
{{!-- Text: Plain text version of your email. If not provided, HTML tags will be stripped from the HTML version for the
|
{{!-- Text: Plain text version of your email. If not provided, HTML tags will be stripped from the HTML version for the
|
||||||
text version. --}}
|
text version. --}}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
|
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
|
||||||
<p>Здравейте,</p>
|
<p>Здравей,</p>
|
||||||
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
|
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
|
||||||
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
||||||
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport"
|
<meta name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0">
|
content="width=device-width, initial-scale=1.0">
|
||||||
<title>ССС известия</title>
|
<title>ССОМ известия</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
||||||
© 2024 ССС. All rights reserved.
|
© 2024 ССОМ. Openly licensed.
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{!-- Subject: ССС: Нови назначени смени--}}
|
{{!-- Subject: ССОМ: Нови назначени смени--}}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Здравейте, {{publisherFirstName}} {{publisherLastName}}!</h2>
|
<h2>Здравей {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||||
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
||||||
<p>За месец {{month}} имате следните смени:</p>
|
<p>За месец {{month}} имате следните смени:</p>
|
||||||
<div>
|
<div>
|
||||||
|
@ -267,3 +267,10 @@ iframe {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.rbc-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center; /* Optional: center buttons in the group */
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@
|
|||||||
"pages/cart/locations/[id].tsx.typed",
|
"pages/cart/locations/[id].tsx.typed",
|
||||||
"components/location/LocationForm.js",
|
"components/location/LocationForm.js",
|
||||||
"pages/cart/locations/[id].tsx.old",
|
"pages/cart/locations/[id].tsx.old",
|
||||||
"components/publisher/ShiftsList.js"
|
"components/publisher/ShiftsList.js",
|
||||||
|
"src/helpers/data.js"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
console.log('Service Worker Loaded...')
|
console.log('Service Worker Loaded...')
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
try {
|
||||||
|
if (event.request.url.includes('/api/auth/callback/')) {
|
||||||
|
// Use network only strategy for auth routes, or bypass SW completely
|
||||||
|
event.respondWith(fetch(event.request));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// other caching strategies...
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.addEventListener('push', function (event) {
|
self.addEventListener('push', function (event) {
|
||||||
console.log('Push message', event)
|
console.log('Push message', event)
|
||||||
if (!(self.Notification && self.Notification.permission === 'granted')) {
|
if (!(self.Notification && self.Notification.permission === 'granted')) {
|
||||||
|
Reference in New Issue
Block a user