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
|
||||
# mysql
|
||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||
|
||||
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
||||
# // 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_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_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
|
||||
@ -45,18 +48,18 @@ GITHUB_SECRET=
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
|
||||
# EMAIL_BYPASS_TO=mwitnessing@gmail.com
|
||||
EMAIL_SENDER='"ССОМ" <mwitnessing@gmail.com>'
|
||||
# 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_PORT=2525
|
||||
MAILTRAP_USER=8ec69527ff2104
|
||||
MAILTRAP_PASS=c7bc05f171c96c
|
||||
|
||||
GMAIL_EMAIL_USERNAME=
|
||||
GMAIL_EMAIL_APP_PASS=
|
||||
|
||||
TELEGRAM_BOT=false
|
||||
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@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_CERT=./certificates/localhost.pem
|
||||
|
@ -7,9 +7,15 @@ NEXT_PUBLIC_PUBLIC_URL= https://sofia.mwitnessing.com
|
||||
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
||||
# DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||
|
||||
|
||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||
MAILTRAP_USER=api
|
||||
MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
||||
EMAIL_BYPASS_TO=
|
||||
EMAIL_SENDER='"Специално Свидетелстване София" <mwitnessing@gmail.com>'
|
||||
EMAIL_SERVICE=gmail
|
||||
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
|
||||
DATABASE=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
|
||||
|
||||
APPLE_ID=
|
||||
APPLE_TEAM_ID=
|
||||
APPLE_PRIVATE_KEY=
|
||||
APPLE_KEY_ID=
|
||||
|
||||
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
|
||||
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
|
||||
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
|
||||
|
||||
FACEBOOK_ID=
|
||||
FACEBOOK_SECRET=
|
||||
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
|
||||
# GOOGLE_SECRET=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
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
EMAIL_SERVICE=mailtrap
|
||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||
MAILTRAP_USER=api
|
||||
|
@ -19,7 +19,7 @@ services:
|
||||
- GIT_USERNAME=deploy
|
||||
- GIT_PASSWORD=L3Kr2R438u4F7
|
||||
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
|
||||
stdin_open: true
|
||||
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-lock.json /app/package-lock.json || echo "Rsync failed: Issue copying package-lock.json"
|
||||
rm -rf /app/node_modules
|
||||
cd /app
|
||||
npm install --no-audit --no-fund --no-optional --omit=optional
|
||||
yes | npx prisma generate
|
||||
else
|
||||
echo "Package files have not changed. Skipping package installation."
|
||||
fi
|
||||
|
||||
cd /app
|
||||
npm install --no-audit --no-fund --no-optional --omit=optional
|
||||
npx next build
|
||||
|
||||
# Clean up
|
||||
rm -rf /tmp/clone
|
||||
echo "Update process completed."
|
||||
|
@ -4,14 +4,14 @@ import { SignJWT } from "jose"
|
||||
import { createPrivateKey } from "crypto"
|
||||
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`
|
||||
console.log(`
|
||||
Creates a JWT from the components found at Apple.
|
||||
By default, the JWT has a 6 months expiry date.
|
||||
Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
|
||||
|
||||
Usage:
|
||||
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
|
||||
APPLE_ID=com.mwhitnessing.sofia
|
||||
APPLE_APP_ID=com.mwhitnessing.sofia
|
||||
APPLE_TEAM_ID=XC57P9SXDK
|
||||
APPLE_KEY_ID=TB3V355G5Y
|
||||
APPLE_KEY
|
||||
@ -37,45 +37,45 @@ eyJhbGciOiJFUzI1NiIsImtpZCI6IlRCM1YzNTVHNVkifQ.eyJhdWQiOiJodHRwczovL2FwcGxlaWQuY
|
||||
--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 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,
|
||||
const {
|
||||
team_id,
|
||||
iss = team_id,
|
||||
|
||||
private_key,
|
||||
private_key,
|
||||
|
||||
client_id,
|
||||
sub = client_id,
|
||||
client_id,
|
||||
sub = client_id,
|
||||
|
||||
key_id,
|
||||
kid = key_id,
|
||||
key_id,
|
||||
kid = key_id,
|
||||
|
||||
expires_in = 86400 * 180,
|
||||
exp = Math.ceil(Date.now() / 1000) + expires_in,
|
||||
} = args
|
||||
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(`
|
||||
/**
|
||||
* 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")))}`)
|
||||
.setAudience("https://appleid.apple.com")
|
||||
.setIssuer(iss)
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(expirationTime)
|
||||
.setSubject(sub)
|
||||
.setProtectedHeader({ alg: "ES256", kid })
|
||||
.sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
|
||||
}
|
12
_doc/ToDo.md
12
_doc/ToDo.md
@ -207,6 +207,18 @@ push notifications
|
||||
store replacement
|
||||
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;
|
||||
}
|
||||
|
||||
await fetch('/api/notification', {
|
||||
await fetch('/api/notify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@ -219,6 +219,11 @@ function PwaManager() {
|
||||
<span className="align-middle">Телеграм</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4" target="_blank">
|
||||
<span className="align-middle">Apple sign-in</span>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -10,6 +10,8 @@ import { bgBG } from '../x-date-pickers/locales/bgBG';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
const common = require('src/helpers/common');
|
||||
//todo import Availability type from prisma schema
|
||||
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns';
|
||||
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
@ -25,6 +27,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
indexUrl: "/cart/availabilities"
|
||||
};
|
||||
|
||||
const id = parseInt(router.query.id);
|
||||
//coalsce existingItems to empty array
|
||||
existingItems = existingItems || [];
|
||||
|
||||
@ -74,7 +77,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
|
||||
|
||||
const fetchItemFromDB = async () => {
|
||||
const id = parseInt(router.query.id);
|
||||
if (existingItems.length == 0 && id) {
|
||||
try {
|
||||
const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
|
||||
@ -183,59 +185,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
return groupedIntervals;
|
||||
}
|
||||
|
||||
// // const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport;
|
||||
// // const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport;
|
||||
// function createAvailabilityFromGroup(group) {
|
||||
// let startTime = new Date(day);
|
||||
// startTime.setHours(group[0].startTime.getHours(), group[0].startTime.getMinutes(), group[0].startTime.getSeconds(), 0);
|
||||
|
||||
// let endTime = new Date(day);
|
||||
// endTime.setHours(group[group.length - 1].endTime.getHours(), group[group.length - 1].endTime.getMinutes(), group[group.length - 1].endTime.getSeconds(), 0);
|
||||
|
||||
|
||||
// return {
|
||||
// name: common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime),
|
||||
// publisherId: publisher.id,
|
||||
// startTime: startTime,
|
||||
// endTime: endTime,
|
||||
// isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport,
|
||||
// isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport,
|
||||
// dayofweek: common.getDayOfWeekNameEnEnumForDate(day.getDay()),
|
||||
// repeatWeekly: doRepeat,
|
||||
// dayOfMonth: doRepeat ? null : startTime.getDate(),
|
||||
// endDate: doRepeat ? repeatUntil : null,
|
||||
// dateOfEntry: new Date(),
|
||||
// };
|
||||
// }
|
||||
|
||||
// function updateAvailabilityFromGroup(availability, group) {
|
||||
// availability.startTime.setTime(group[0].startTime);
|
||||
// availability.endTime.setTime(group[group.length - 1].endTime);
|
||||
// availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
|
||||
|
||||
// availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
|
||||
// availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport;
|
||||
|
||||
// delete availability.weekOfMonth;
|
||||
// if (doRepeat) {
|
||||
// availability.repeatWeekly = true;
|
||||
// availability.dayOfMonth = null;
|
||||
// availability.weekOfMonth = 0;
|
||||
// availability.endDate = repeatUntil;
|
||||
// } else {
|
||||
// availability.repeatWeekly = false;
|
||||
// availability.dayOfMonth = availability.startTime.getDate();
|
||||
// availability.endDate = null;
|
||||
// }
|
||||
|
||||
// availability.dateOfEntry = new Date();
|
||||
// if (availability.parentAvailabilityId) {
|
||||
// availability.parentAvailability = { connect: { id: parentAvailabilityId } };
|
||||
// }
|
||||
// delete availability.parentAvailabilityId;
|
||||
|
||||
// return availability;
|
||||
// }
|
||||
// Common function to set shared properties
|
||||
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
||||
let startTime = new Date(availability.startTime || day);
|
||||
@ -263,7 +212,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
availability.dayOfMonth = startTime.getDate();
|
||||
availability.endDate = null;
|
||||
}
|
||||
|
||||
availability.isFromPreviousMonth = false;
|
||||
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);
|
||||
|
||||
//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 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) {
|
||||
let slotStart = new Date(currentTime);
|
||||
let slotEnd = new Date(currentTime + increment * 60000); // increment is in minutes
|
||||
while (isBefore(currentTime, end)) {
|
||||
let slotStart = normalizeTime(currentTime, baseDate);
|
||||
let slotEnd = normalizeTime(addMinutes(currentTime, increment), baseDate);
|
||||
|
||||
const isChecked = items.some(item =>
|
||||
item.startTime && item.endTime &&
|
||||
(slotStart.getTime() < item.endTime.getTime()) &&
|
||||
(slotEnd.getTime() > item.startTime.getTime())
|
||||
);
|
||||
const isChecked = items.some(item => {
|
||||
let itemStart = item.startTime ? normalizeTime(new Date(item.startTime), baseDate) : null;
|
||||
let itemEnd = item.endTime ? normalizeTime(new Date(item.endTime), baseDate) : null;
|
||||
|
||||
return itemStart && itemEnd &&
|
||||
(slotStart.getTime() < itemEnd.getTime()) &&
|
||||
(slotEnd.getTime() > itemStart.getTime());
|
||||
});
|
||||
|
||||
slots.push({
|
||||
startTime: slotStart,
|
||||
@ -355,10 +307,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
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) {
|
||||
slots[0].isFirst = true;
|
||||
slots[slots.length - 1].isLast = true;
|
||||
@ -369,6 +320,16 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
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 [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
||||
const handleAllDayChange = (e) => {
|
||||
@ -467,13 +428,14 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
<ToastContainer></ToastContainer>
|
||||
<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">
|
||||
{editMode ? "Редактирай" : "Нова"} възможност
|
||||
{editMode ? "Редактирай" : "Нова"} възможност: {common.getDateFormatedShort(day)}
|
||||
</h3>
|
||||
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} localeText={bgBG} adapterLocale={bg}>
|
||||
<div className="mb-2">
|
||||
{/* <div className="mb-2">
|
||||
<DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} />
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="checkbox-container">
|
||||
|
@ -10,7 +10,7 @@ const common = require('src/helpers/common');
|
||||
|
||||
|
||||
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
||||
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const [assignments, setAssignments] = useState(shift.assignments);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [useFilterDate, setUseFilterDate] = useState(true);
|
||||
@ -24,24 +24,14 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
||||
}, [shift.assignments]);
|
||||
|
||||
const handleShiftClick = (shiftId) => {
|
||||
// console.log("onShiftSelect prop:", onShiftSelect);
|
||||
// console.log("Shift clicked:", shift);
|
||||
//shift.selectedPublisher = selectedPublisher;
|
||||
if (onShiftSelect) {
|
||||
onShiftSelect(shift);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePublisherClick = (publisher) => {
|
||||
|
||||
//toggle selected
|
||||
// if (selectedPublisher != null) {
|
||||
// setSelectedPublisher(null);
|
||||
// }
|
||||
// else {
|
||||
setSelectedPublisher(publisher);
|
||||
|
||||
|
||||
console.log("Publisher clicked:", publisher, "selected publisher:", selectedPublisher);
|
||||
shift.selectedPublisher = publisher;
|
||||
if (onShiftSelect) {
|
||||
@ -54,6 +44,17 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
||||
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) => {
|
||||
try {
|
||||
console.log("Removing assignment with id:", id);
|
||||
@ -100,137 +101,162 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
||||
} 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 (
|
||||
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
||||
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
|
||||
{/* Time Window Header */}
|
||||
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
||||
<span className="text-lg font-semibold">
|
||||
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
||||
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
|
||||
</span>
|
||||
<>{!isDeleted && (
|
||||
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
||||
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
|
||||
{/* Time Window Header */}
|
||||
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
||||
<span className="flex text-lg font-semibold">
|
||||
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
||||
{/* {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 */}
|
||||
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
|
||||
копирай имената {/* Placeholder for Copy icon */}
|
||||
</button>
|
||||
{/* Hint Message */}
|
||||
{showCopyHint && (
|
||||
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
|
||||
Имената са копирани
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Copy All Names Button */}
|
||||
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
|
||||
копирай имената {/* Placeholder for Copy icon */}
|
||||
</button>
|
||||
{/* Hint Message */}
|
||||
{showCopyHint && (
|
||||
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
|
||||
Имената са копирани
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Assignments */}
|
||||
{assignments.map((ass, index) => {
|
||||
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
||||
{/* Assignments */}
|
||||
{assignments.map((ass, index) => {
|
||||
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
||||
|
||||
// Determine border styles
|
||||
let borderStyles = '';
|
||||
let canTransport = false;
|
||||
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
||||
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 ';
|
||||
// Determine border styles
|
||||
let borderStyles = '';
|
||||
let canTransport = false;
|
||||
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
||||
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers
|
||||
}
|
||||
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 {
|
||||
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.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
|
||||
// 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
|
||||
// }
|
||||
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 || 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>
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
{/* 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">
|
||||
{/* Add Button */}
|
||||
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
|
||||
добави {/* Placeholder for Add icon */}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex space-x-2 items-center">
|
||||
{/* Add Button */}
|
||||
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
|
||||
добави участник{/* Placeholder for Add icon */}
|
||||
</button>
|
||||
{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)}
|
||||
*/}
|
||||
<Modal isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
forDate={new Date(shift.startTime)}
|
||||
useFilterDate={useFilterDate}
|
||||
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
||||
<Modal isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
forDate={new Date(shift.startTime)}
|
||||
useFilterDate={useFilterDate}
|
||||
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
||||
|
||||
<PublisherSearchBox
|
||||
selectedId={null}
|
||||
isFocused={isModalOpen}
|
||||
filterDate={useFilterDate ? new Date(shift.startTime) : null}
|
||||
onChange={(publisher) => {
|
||||
// Add publisher as assignment logic
|
||||
setIsModalOpen(false);
|
||||
addAssignment(publisher, shift.id);
|
||||
}}
|
||||
showAllAuto={true}
|
||||
showSearch={true}
|
||||
showList={false}
|
||||
/>
|
||||
</Modal>
|
||||
</div >
|
||||
<PublisherSearchBox
|
||||
selectedId={null}
|
||||
isFocused={isModalOpen}
|
||||
filterDate={useFilterDate ? new Date(shift.startTime) : null}
|
||||
onChange={(publisher) => {
|
||||
// Add publisher as assignment logic
|
||||
setIsModalOpen(false);
|
||||
addAssignment(publisher, shift.id);
|
||||
}}
|
||||
showAllAuto={true}
|
||||
showSearch={true}
|
||||
showList={false}
|
||||
/>
|
||||
</Modal>
|
||||
</div >
|
||||
)}</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,13 @@ import { MdToday } from 'react-icons/md';
|
||||
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
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
|
||||
moment.locale('bg');
|
||||
@ -40,11 +46,13 @@ const messages = {
|
||||
const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
|
||||
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 [displayedEvents, setDisplayedEvents] = useState(evts); // Events to display in the calendar
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
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 start = new Date();
|
||||
start.setDate(1); // Set to the first day of the current month
|
||||
@ -162,6 +170,31 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
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
|
||||
const minHour = 8; // 8:00 AM
|
||||
@ -172,12 +205,13 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
maxTime.setHours(maxHour, 0, 0);
|
||||
const totalHours = maxHour - minHour;
|
||||
|
||||
const handleSelect = ({ start, end }) => {
|
||||
const handleSelect = ({ mode, start, end }) => {
|
||||
const startdate = typeof start === 'string' ? new Date(start) : start;
|
||||
const enddate = typeof end === 'string' ? new Date(end) : end;
|
||||
|
||||
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
|
||||
if (startdate.toDateString() !== enddate.toDateString()) {
|
||||
@ -198,52 +232,25 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
setDate(start);
|
||||
|
||||
// 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());
|
||||
// const existingEvents = evts?.filter(event => {
|
||||
// return event.publisherId === publisherId &&
|
||||
// new Date(event.startTime).getFullYear() === start.getFullYear() &&
|
||||
// new Date(event.startTime).getMonth() === start.getMonth() &&
|
||||
// new Date(event.startTime).getDate() === start.getDate();
|
||||
// });
|
||||
//ToDo: properly fix this. filterEvents does not return the expcted results
|
||||
let existingEvents = filterEvents(evts, publisherId, startdate);
|
||||
// if existingEvents is empty - create new with the selected range
|
||||
// if (existingEvents.length === 0) {
|
||||
// existingEvents = [{ startTime: start, endTime: end }];
|
||||
// }
|
||||
console.log("handleSelect: " + existingEvents);
|
||||
setSelectedEvents(existingEvents);
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
const handleEventClick = (event) => {
|
||||
if (event.type === "assignment") return;
|
||||
handleSelect({ start: event.startTime, end: event.endTime });
|
||||
// Handle event click
|
||||
// const eventForEditing = {
|
||||
// ...event,
|
||||
// startTime: new Date(event.startTime),
|
||||
// endTime: new Date(event.endTime),
|
||||
// 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);
|
||||
|
||||
//select the whole day
|
||||
let start = new Date(event.startTime);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
let end = new Date(event.startTime);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
handleSelect({ mode: 'select', start: start, end: end });
|
||||
};
|
||||
|
||||
const handleDialogClose = async (dialogEvent) => {
|
||||
@ -256,10 +263,8 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
newEvents.forEach(event => {
|
||||
event.startTime = new Date(event.startTime);
|
||||
event.endTime = new Date(event.endTime);
|
||||
|
||||
});
|
||||
setEvents(newEvents);
|
||||
|
||||
}
|
||||
|
||||
console.log("handleSave: ", dialogEvent);
|
||||
@ -269,6 +274,56 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
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 [isHovered, setIsHovered] = useState(false);
|
||||
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 }) => {
|
||||
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 (
|
||||
<> <div {...handlers} className="flex flex-col"
|
||||
>
|
||||
@ -488,10 +509,15 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
|
||||
components={{
|
||||
event: EventWrapper,
|
||||
toolbar: CustomToolbar,
|
||||
view: CustomEventAgenda,
|
||||
agenda: {
|
||||
event: CustomEventAgenda
|
||||
},
|
||||
// ... other custom components
|
||||
}}
|
||||
eventPropGetter={(eventStyleGetter)}
|
||||
date={date}
|
||||
showAllEvents={true}
|
||||
onNavigate={setDate}
|
||||
className="rounded-lg shadow-lg"
|
||||
/>
|
||||
|
@ -9,56 +9,63 @@ import Body from 'next/document'
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { set } from "date-fns"
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const router = useRouter()
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
export default function Layout({ children }) {
|
||||
const router = useRouter();
|
||||
|
||||
// auto resize for tablets: disabled.
|
||||
// useEffect(() => {
|
||||
// // Function to check and set the state based on window width
|
||||
// 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);
|
||||
// }, []);
|
||||
// Assuming that during SSR, we don't want the sidebar to be open.
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
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 = () => {
|
||||
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 (
|
||||
|
||||
// <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="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 }} />
|
||||
<Sidebar isSidebarOpen={isSidebarOpen} toggleSidebar={toggleSidebar} />
|
||||
<main className={`w-full pr-10 transition-all h-[90vh] duration-300 ${isSidebarOpen ? 'ml-60 ' : 'ml-6'}`}>
|
||||
{children}
|
||||
<main className={`flex-1 transition-all duration-300 ${marginLeftClass}`}>
|
||||
<div className="p-4 mx-auto pr-8 pl-0">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/* <div className="justify-end items-center text-center ">
|
||||
<Footer />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import DayOfWeek from "../DayOfWeek";
|
||||
import TextEditor from "../TextEditor";
|
||||
import FileUploadWithPreview from 'components/FileUploadWithPreview ';
|
||||
|
||||
import ProtectedRoute, { serverSideAuth } from "../..//components/protectedRoute";
|
||||
import ProtectedRoute, { serverSideAuth } from "../../components/protectedRoute";
|
||||
import { UserRole } from "@prisma/client";
|
||||
|
||||
const common = require('src/helpers/common');
|
||||
|
@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
import ProtectedRoute, { serverSideAuth } from "../../components/protectedRoute";
|
||||
|
||||
//add months to date. works with negative numbers and numbers > 12
|
||||
export function addMonths(numOfMonths, date) {
|
||||
@ -53,6 +54,23 @@ export default function PublisherCard({ publisher }) {
|
||||
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 ? (
|
||||
// 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" /> */}
|
||||
</svg>
|
||||
</button>
|
||||
<ProtectedRoute>
|
||||
|
||||
<button onClick={() => handleLoginAs(publisher.id)}>Login as</button>
|
||||
</ProtectedRoute>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.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 Link from "next/link";
|
||||
import DayOfWeek from "../DayOfWeek";
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import { ReportType } from "@prisma/client";
|
||||
const common = require('src/helpers/common');
|
||||
|
||||
import { useSession } from "next-auth/react"
|
||||
@ -97,6 +97,7 @@ export default function ExperienceForm({ publisherId, assgnmentId, existingItem,
|
||||
e.preventDefault();
|
||||
item.publisher = { connect: { id: pubId } };
|
||||
item.location = { connect: { id: parseInt(item.locationId) } };
|
||||
item.type = ReportType.Experience;
|
||||
delete item.locationId;
|
||||
|
||||
try {
|
||||
|
@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import DayOfWeek from "../DayOfWeek";
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import { ReportType } from "@prisma/client";
|
||||
const common = require('src/helpers/common');
|
||||
|
||||
import { useSession } from "next-auth/react"
|
||||
@ -55,7 +55,7 @@ export default function FeedbackForm({ publisherId, onDone }) {
|
||||
assignmentId: 0,
|
||||
publisherId: publisherId,
|
||||
date: new Date(),
|
||||
placementCount: 0,
|
||||
placementCount: 1,
|
||||
videoCount: 0,
|
||||
returnVisitInfoCount: 0,
|
||||
conversationCount: 0
|
||||
@ -73,6 +73,8 @@ export default function FeedbackForm({ publisherId, onDone }) {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
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;
|
||||
|
||||
try {
|
||||
|
@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { useSession } from "next-auth/react"
|
||||
import { ReportType } from "@prisma/client";
|
||||
|
||||
const common = require('src/helpers/common');
|
||||
|
||||
@ -89,6 +90,7 @@ export default function ReportForm({ shiftId, existingItem, onDone }) {
|
||||
item.publisher = { connect: { id: publisherId } };
|
||||
item.shift = { connect: { id: parseInt(item.shiftId) } };
|
||||
item.date = new Date(item.date);
|
||||
item.type = ReportType.Report;
|
||||
delete item.publisherId;
|
||||
delete item.shiftId;
|
||||
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"
|
||||
style={{ transform: isSidebarOpen ? `translateX(${sidebarWidth - 64}px)` : 'translateX(-20px)' }}>☰</button>
|
||||
<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)` }}>
|
||||
<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>
|
||||
@ -163,7 +163,7 @@ function UserSection({ session }) {
|
||||
|
||||
function SignInButton() {
|
||||
return (
|
||||
<div className="items-center py-2" onClick={() => signIn()}>
|
||||
<div className="items-center py-2 font-bold" onClick={() => signIn()}>
|
||||
<button>Впишете се</button>
|
||||
</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, // ?
|
||||
publicExcludes: ["!_error*.js"], //?
|
||||
|
||||
disable: process.env.NODE_ENV === 'development',
|
||||
//disable: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
|
||||
module.exports = withPWA({
|
||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -4802,6 +4802,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"clean-stack": "^2.0.0",
|
||||
"indent-string": "^4.0.0"
|
||||
@ -5771,6 +5772,7 @@
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@ -5784,6 +5786,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -8546,7 +8549,8 @@
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
@ -8888,6 +8892,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@ -8915,6 +8920,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
|
||||
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@ -10943,6 +10949,7 @@
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
@ -10954,6 +10961,7 @@
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@ -13870,6 +13878,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
},
|
||||
@ -15871,6 +15880,7 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
@ -15879,12 +15889,14 @@
|
||||
"node_modules/spdx-exceptions": {
|
||||
"version": "2.5.0",
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"spdx-exceptions": "^2.1.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
@ -15893,7 +15905,8 @@
|
||||
"node_modules/spdx-license-ids": {
|
||||
"version": "3.0.17",
|
||||
"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": {
|
||||
"version": "0.8.2",
|
||||
@ -17442,6 +17455,7 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pwwa",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"description": "JW PW Web App",
|
||||
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
||||
@ -12,7 +12,7 @@
|
||||
"debug": "node server.js",
|
||||
"debug-env": "dotenv -e .env.$APP_ENV -- nodemon --inspect server.js",
|
||||
"nodeenv": "dotenv -e .env.$APP_ENV -- node server.js",
|
||||
"prod": "npx next build && dotenv -e .env.production -- node server.js",
|
||||
"prod": "dotenv -e .env.production -- node server.js",
|
||||
"build": "next build",
|
||||
"buildWin": "npm run build",
|
||||
"start": "next start",
|
||||
@ -113,4 +113,4 @@
|
||||
"depcheck": "^1.4.7",
|
||||
"prisma": "^5.12.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ const common = require("../../../src/helpers/common");
|
||||
import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt'
|
||||
|
||||
|
||||
console.log("appleID:", process.env.APPLE_APP_ID);
|
||||
// console.log(process.env.EMAIL_SERVER)
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
@ -41,10 +42,10 @@ export const authOptions: NextAuthOptions = {
|
||||
}
|
||||
}
|
||||
}),
|
||||
AppleProvider({
|
||||
clientId: process.env.APPLE_ID,
|
||||
clientSecret: process.env.APPLE_SECRET
|
||||
}),
|
||||
// AppleProvider({
|
||||
// clientId: process.env.APPLE_APP_ID,
|
||||
// clientSecret: process.env.APPLE_SECRET
|
||||
// }),
|
||||
// AzureADProvider({
|
||||
// clientId: process.env.AZURE_AD_CLIENT_ID,
|
||||
// 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 { createRouter, expressWrapper } from "next-connect";
|
||||
const common = require('../../src/helpers/common');
|
||||
const data = require('../../src/helpers/data');
|
||||
const emailHelper = require('../../src/helpers/email');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const CON = require("../../src/helpers/const");
|
||||
import { EventLogType } from "@prisma/client";
|
||||
|
||||
import fs from 'fs';
|
||||
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)}`;
|
||||
@ -201,7 +212,7 @@ export default async function handler(req, res) {
|
||||
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: {
|
||||
email: token.email
|
||||
}
|
||||
@ -209,13 +220,14 @@ export default async function handler(req, res) {
|
||||
|
||||
switch (action) {
|
||||
case "sendCoverMeRequestByEmail":
|
||||
// Send CoverMe request to the user
|
||||
// Send CoverMe request to the users
|
||||
//get from POST data: shiftId, assignmentId, date
|
||||
//let shiftId = req.body.shiftId;
|
||||
let assignmentId = req.body.assignmentId;
|
||||
let date = req.body.date;
|
||||
|
||||
console.log("User: " + user.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " " + date);
|
||||
let toSubscribed = req.body.toSubscribed;
|
||||
let toAvailable = req.body.toAvailable;
|
||||
|
||||
|
||||
let assignment = await prisma.assignment.findUnique({
|
||||
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
|
||||
let newPublicGuid = uuidv4();
|
||||
@ -246,29 +260,55 @@ export default async function handler(req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
//get all subscribed publisers
|
||||
const subscribedPublishers = await prisma.publisher.findMany({
|
||||
where: {
|
||||
isSubscribedToCoverMe: true
|
||||
let subscribedPublishers = [], availablePublishers = [];
|
||||
if (toSubscribed) {
|
||||
//get all subscribed publisers
|
||||
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
|
||||
for (let i = 0; i < subscribedPublishers.length; i++) {
|
||||
if (subscribedPublishers[i].id == user.id) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < pubsToSend.length; i++) {
|
||||
|
||||
//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 = {
|
||||
user: user,
|
||||
user: publisher,
|
||||
shiftId: assignment.shiftId,
|
||||
acceptUrl: acceptUrl,
|
||||
prefix: user.isMale ? "Брат" : "Сестра",
|
||||
firstName: subscribedPublishers[i].firstName,
|
||||
lastName: subscribedPublishers[i].lastName,
|
||||
email: subscribedPublishers[i].email,
|
||||
firstName: pubsToSend[i].firstName,
|
||||
lastName: pubsToSend[i].lastName,
|
||||
email: pubsToSend[i].email,
|
||||
placeName: assignment.shift.cartEvent.location.name,
|
||||
dateStr: common.getDateFormated(assignment.shift.startTime),
|
||||
time: common.formatTimeHHmm(assignment.shift.startTime),
|
||||
@ -276,8 +316,8 @@ export default async function handler(req, res) {
|
||||
};
|
||||
let results = emailHelper.SendEmailHandlebars(
|
||||
{
|
||||
name: subscribedPublishers[i].firstName + " " + subscribedPublishers[i].lastName,
|
||||
email: subscribedPublishers[i].email
|
||||
name: pubsToSend[i].firstName + " " + pubsToSend[i].lastName,
|
||||
email: pubsToSend[i].email
|
||||
}, "coverMe", model);
|
||||
// if (results) {
|
||||
// console.log("Error sending email: " + error);
|
||||
@ -285,10 +325,12 @@ export default async function handler(req, res) {
|
||||
//}
|
||||
|
||||
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;
|
||||
default:
|
||||
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 filter = req.query.filter;
|
||||
let day: Date, monthInfo: any;
|
||||
let day: Date;
|
||||
let isExactTime;
|
||||
if (req.query.date) {
|
||||
day = new Date(req.query.date);
|
||||
@ -42,6 +42,7 @@ export default async function handler(req, res) {
|
||||
isExactTime = true;
|
||||
}
|
||||
|
||||
let monthInfo = common.getMonthDatesInfo(day);
|
||||
const searchText = req.query.searchText?.normalize('NFC');
|
||||
|
||||
try {
|
||||
@ -123,10 +124,10 @@ export default async function handler(req, res) {
|
||||
const availabilities = req.body;
|
||||
//! console.log("createAvailabilities: " + JSON.stringify(availabilities));
|
||||
try {
|
||||
await prisma.availability.createMany({
|
||||
let createResults = await prisma.availability.createMany({
|
||||
data: availabilities
|
||||
});
|
||||
res.status(200).json({ "message": "ok" });
|
||||
res.status(200).json({ "message": "ok", "results": createResults });
|
||||
} catch (error) {
|
||||
console.error("Error creating availabilities: " + error);
|
||||
res.status(500).json({ error });
|
||||
@ -161,8 +162,9 @@ export default async function handler(req, res) {
|
||||
res.status(200).json(publishers);
|
||||
break;
|
||||
case "filterPublishersNew":
|
||||
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
|
||||
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);
|
||||
break;
|
||||
|
||||
@ -219,6 +221,7 @@ export default async function handler(req, res) {
|
||||
|
||||
res.status(200).json(shiftsForDate);
|
||||
break;
|
||||
|
||||
case "copyOldAvailabilities":
|
||||
//get all publishers that don't have availabilities for the current month
|
||||
monthInfo = common.getMonthDatesInfo(day);
|
||||
@ -282,12 +285,12 @@ export default async function handler(req, res) {
|
||||
type: AvailabilityType.Monthly,
|
||||
isFromPreviousMonth: true,
|
||||
name: avail.name || "старо предпочитание",
|
||||
// parentAvailabilityId: avail.id
|
||||
parentAvailability: {
|
||||
connect: {
|
||||
id: avail.id
|
||||
}
|
||||
}
|
||||
parentAvailabilityId: avail.id,
|
||||
// parentAvailability: {
|
||||
// connect: {
|
||||
// id: avail.id
|
||||
// }
|
||||
// },
|
||||
}
|
||||
await prisma.availability.create({ data: data });
|
||||
|
||||
@ -330,12 +333,11 @@ export default async function handler(req, res) {
|
||||
|
||||
case "updateShifts":
|
||||
//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 updated = await prisma.shift.updateMany({
|
||||
where: {
|
||||
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),
|
||||
}
|
||||
},
|
||||
@ -347,6 +349,43 @@ export default async function handler(req, res) {
|
||||
res.status(200).json({ "message": "ok" });
|
||||
|
||||
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:
|
||||
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) {
|
||||
|
||||
// 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;
|
||||
export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true, includeOldAvailabilities = false) {
|
||||
return data.filterPublishersNew(selectFields, filterDate, isExactTime, isForTheMonth, isWithStats, includeOldAvailabilities);
|
||||
}
|
||||
|
||||
// availabilites filter:
|
||||
@ -759,7 +551,8 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true
|
||||
endTime: true,
|
||||
isPublished: true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1062,7 +855,11 @@ async function getCalendarEvents(publisherId, date, availabilities = true, assig
|
||||
});
|
||||
}
|
||||
if (assignments) {
|
||||
publisher.assignments?.forEach(item => {
|
||||
//only published shifts
|
||||
|
||||
publisher.assignments?.filter(
|
||||
assignment => assignment.shift.isPublished
|
||||
).forEach(item => {
|
||||
result.push({
|
||||
...item,
|
||||
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') {
|
||||
// const { year, month } = req.query;
|
||||
const { year, month } = req.query;
|
||||
|
||||
// let monthIndex = parseInt(month as string) - 1;
|
||||
// const monthInfo = common.getMonthDatesInfo(new Date(year, month, 1));
|
||||
// let fromDate = monthInfo.firstMonday;
|
||||
// const toDate = monthInfo.lastSunday;
|
||||
//ToDo: maybe we don't need that anymore as we are publishing the shifts and show all published shifts
|
||||
let fromDate = new Date();
|
||||
fromDate.setDate(fromDate.getDate() - 1);
|
||||
fromDate.setHours(0, 0, 0, 0);
|
||||
let toDate = new Date(fromDate);
|
||||
toDate.setDate(toDate.getDate() + 40);
|
||||
if (year && month) {
|
||||
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 {
|
||||
|
||||
@ -93,7 +96,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
isPublished: true,
|
||||
startTime: {
|
||||
gte: fromDate,
|
||||
lt: toDate,
|
||||
//lt: toDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
@ -131,12 +134,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
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 = {
|
||||
date: date,
|
||||
placeOfEvent: shift.cartEvent.location.name,
|
||||
time: time,
|
||||
requiresTransport: shift.requiresTransport,
|
||||
//bold the text after - in the notes
|
||||
notes: notes,
|
||||
notes_bold: notes_bold,
|
||||
@ -159,6 +171,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
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
|
||||
const monthlySchedule = {
|
||||
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");
|
||||
continue;
|
||||
}
|
||||
|
||||
let weekday = common.getDayOfWeekName(shift.date);
|
||||
let monthName = common.getMonthName(shift.date.getMonth());
|
||||
weekday = weekday.charAt(0).toUpperCase() + weekday.slice(1);
|
||||
|
@ -76,15 +76,15 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
const [modalPub, setModalPub] = useState(null);
|
||||
|
||||
// ------------------ no assignments checkbox ------------------
|
||||
const [isCheckboxChecked, setIsCheckboxChecked] = useState(false);
|
||||
const [filterShowWithoutAssignments, setFilterShowWithoutAssignments] = useState(false);
|
||||
const handleCheckboxChange = (event) => {
|
||||
setIsCheckboxChecked(!isCheckboxChecked); // Toggle the checkbox state
|
||||
setFilterShowWithoutAssignments(!filterShowWithoutAssignments); // Toggle the checkbox state
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("checkbox checked: " + isCheckboxChecked);
|
||||
console.log("checkbox checked: " + filterShowWithoutAssignments);
|
||||
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
|
||||
}, [isCheckboxChecked]); // Dependency array
|
||||
}, [filterShowWithoutAssignments]); // Dependency array
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
|
||||
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 dateStr = 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()}`);
|
||||
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
|
||||
setAvailablePubs(availablePubsForDate);
|
||||
@ -110,7 +110,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
|
||||
setShifts(shiftsForDate);
|
||||
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 => {
|
||||
pub.canTransport = pub.availabilities.some(av =>
|
||||
@ -140,6 +140,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
onChange(selectedDate);
|
||||
}
|
||||
}
|
||||
|
||||
const handleShiftSelection = (selectedShift) => {
|
||||
setSelectedShiftId(selectedShift.id);
|
||||
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)}`);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Layout>
|
||||
@ -621,22 +654,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
</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>
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
<input type="checkbox" className="toggle-checkbox" id="filterIncludeOldAvailabilities" onChange={handleCheckboxChange} />
|
||||
<span className="toggle-slider m-1">със стари предпочитания</span>
|
||||
</label>
|
||||
<ul className="w-full max-w-md">
|
||||
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
|
||||
@ -736,6 +754,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
allPublishersInfo={availablePubs} />
|
||||
))}
|
||||
</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>
|
||||
|
||||
@ -891,6 +915,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import { start } from 'repl';
|
||||
import { filter } from 'jszip';
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
// 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 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: shifts } = await axios.get(url);
|
||||
|
||||
|
@ -22,7 +22,8 @@ const SchedulePage = () => {
|
||||
useEffect(() => {
|
||||
const fetchHtmlContent = async () => {
|
||||
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
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch schedule:", error);
|
||||
|
@ -51,7 +51,8 @@ export default function ImportPage() {
|
||||
desiredShiftsIndex: -1,
|
||||
dataStartIndex: -1,
|
||||
isActiveIndex: -1,
|
||||
pubTypeIndex: -1
|
||||
pubTypeIndex: -1,
|
||||
gender: -1
|
||||
});
|
||||
|
||||
const handleFile = (e) => {
|
||||
@ -111,6 +112,7 @@ export default function ImportPage() {
|
||||
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
|
||||
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
|
||||
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
|
||||
headerRef.current.gender = header.indexOf("Пол");
|
||||
|
||||
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
|
||||
let date;
|
||||
@ -147,12 +149,16 @@ export default function ImportPage() {
|
||||
|
||||
let isOld = false;
|
||||
const row = rawData[i];
|
||||
let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = true, publisherType = PublisherType.Publisher;
|
||||
//const date = new Date(row[0]).toISOS{tring().slice(0, 10);
|
||||
let email, phone, names, dateOfInput, oldAvDeleted = false,
|
||||
isTrained = false, desiredShiftsPerMonth = 4, isActive = true,
|
||||
publisherType = PublisherType.Publisher,
|
||||
isMale = 0
|
||||
;
|
||||
//ToDo: structure all vars above as single object:
|
||||
|
||||
if (mode.mainMode == MODE_PUBLISHERS1) {
|
||||
|
||||
email = row[headerRef.current.emailIndex];
|
||||
|
||||
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
|
||||
// Remove any non-digit characters, except for the leading +
|
||||
//phone = phone.replace(/(?!^\+)\D/g, '');
|
||||
@ -165,11 +171,11 @@ export default function ImportPage() {
|
||||
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
|
||||
dateOfInput = importDate.value || new Date().toISOString();
|
||||
// not empty == true
|
||||
|
||||
isTrained = row[headerRef.current.isTrainedIndex] !== '';
|
||||
isActive = row[headerRef.current.isActiveIndex] == '';
|
||||
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
|
||||
publisherType = row[headerRef.current.pubTypeIndex];
|
||||
isMale = row[headerRef.current.gender].trim().toLowerCase() === 'брат';
|
||||
}
|
||||
else {
|
||||
dateOfInput = common.excelSerialDateToDate(row[0]);
|
||||
@ -183,7 +189,7 @@ export default function ImportPage() {
|
||||
let day = new Date();
|
||||
day.setDate(1); // Set to the first day of the month to avoid overflow
|
||||
if (importDate && importDate.value) {
|
||||
let monthOfIportInfo = common.getMonthInfo(importDate.value);
|
||||
let monthOfIportInfo = common.getMonthInfo(new Date(importDate.value));
|
||||
day = monthOfIportInfo.firstMonday;
|
||||
}
|
||||
|
||||
@ -201,7 +207,7 @@ export default function ImportPage() {
|
||||
let personNames = names.join(' ');
|
||||
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}`);
|
||||
let existingPublisher = responseByName.data[0];
|
||||
if (!existingPublisher) {
|
||||
@ -244,11 +250,8 @@ export default function ImportPage() {
|
||||
} else {
|
||||
data[i - mode.headerRow][4] = "existing";
|
||||
}
|
||||
|
||||
// Log existing publisher
|
||||
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
|
||||
|
||||
|
||||
// Check for other updates
|
||||
const fieldsToUpdate = [
|
||||
{ key: 'email', value: email },
|
||||
@ -256,6 +259,7 @@ export default function ImportPage() {
|
||||
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
||||
{ key: 'isTrained', value: isTrained },
|
||||
{ key: 'isActive', value: isActive },
|
||||
{ key: "isMale", value: isMale },
|
||||
{ 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)
|
||||
+ " updated";
|
||||
} catch (error) {
|
||||
data[i - mode.headerRow][4] = "error updating!";
|
||||
console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error);
|
||||
}
|
||||
}
|
||||
@ -309,6 +314,7 @@ export default function ImportPage() {
|
||||
firstName: firstname,
|
||||
lastName: names[names.length - 1],
|
||||
isActive: isActive,
|
||||
isMale: isMale,
|
||||
isTrained,
|
||||
desiredShiftsPerMonth
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import common from '../../../src/helpers/common';
|
||||
import Modal from 'components/Modal';
|
||||
import ConfirmationModal from 'components/ConfirmationModal';
|
||||
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 { 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 (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
||||
<div className="container ">
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
|
||||
<div className="space-y-4">
|
||||
{assignments && assignments.map((assignment) => (
|
||||
<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 className="border-t border-gray-200">
|
||||
<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>
|
||||
<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)}
|
||||
</dd>
|
||||
</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>
|
||||
<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) => {
|
||||
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.isWithTransport && <LocalShippingIcon style={{ marginLeft: '4px' }} />}
|
||||
</span>
|
||||
@ -101,7 +87,7 @@ export default function MySchedulePage({ assignments }) {
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<div className="bg-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>
|
||||
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<button
|
||||
@ -121,12 +107,8 @@ export default function MySchedulePage({ assignments }) {
|
||||
>
|
||||
Избери Заместник
|
||||
</button>
|
||||
<button
|
||||
className="mr-2 mb-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
onClick={() => searchReplacement(assignment.id)}
|
||||
>
|
||||
Търси заместник
|
||||
</button>
|
||||
|
||||
<SearchReplacement shiftId={assignment.shift.id} assignmentId={assignment.id} />
|
||||
</dd>
|
||||
</div>
|
||||
</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 => {
|
||||
if (assignment.shift && assignment.shift.startTime) {
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import Layout from "../../../components/layout";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import { UserRole } from '@prisma/client';
|
||||
import { Prisma, UserRole } from '@prisma/client';
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
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 filteredPublishers = publishers.filter((publisher) =>
|
||||
const filteredPublishers = allPublishers.filter((publisher) =>
|
||||
publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
publisher.lastName.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]}>
|
||||
<div className="container mx-auto p-4">
|
||||
<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
|
||||
type="text"
|
||||
placeholder="Търси по име, имейл или телефон..."
|
||||
@ -39,26 +42,55 @@ function ContactsPage({ publishers }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredPublishers.map((pub) => (
|
||||
<tr key={pub.id}>
|
||||
<td className="border-b p-4 pl-8" title={pub.lastUpdate}>{pub.firstName} {pub.lastName}</td>
|
||||
<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`} >
|
||||
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td className="border-b p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<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 tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
||||
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredPublishers.map((allPub) => {
|
||||
// Find the publisher in the publishers collection to access statistics
|
||||
const pub = publishers.find(publisher => publisher.id === allPub.id);
|
||||
|
||||
return (
|
||||
<tr key={allPub.id}>
|
||||
<td className="border-b p-4 pl-8" title={allPub.lastUpdate}>{allPub.firstName} {allPub.lastName}</td>
|
||||
{/* Display statistics if publisher is found */}
|
||||
{pub ? (
|
||||
<>
|
||||
<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`}>
|
||||
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td className="border-b p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
@ -71,10 +103,29 @@ function ContactsPage({ publishers }) {
|
||||
|
||||
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) => {
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
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 { 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
|
||||
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 {
|
||||
props: {
|
||||
publishers,
|
||||
allPublishers,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -9,16 +9,35 @@ import { useSession } from "next-auth/react"
|
||||
import common from '../../../src/helpers/common';
|
||||
import Layout from "../../../components/layout";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import { Location, UserRole, ReportType } from "@prisma/client";
|
||||
|
||||
|
||||
export default function Reports() {
|
||||
const [reports, setReports] = useState([]);
|
||||
const [filteredReports, setFilteredReports] = useState([]);
|
||||
|
||||
const router = useRouter();
|
||||
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) => {
|
||||
axiosInstance
|
||||
@ -66,7 +85,7 @@ export default function Reports() {
|
||||
<Layout>
|
||||
<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">
|
||||
<h1 className="text-2xl font-bold text-center">Отчети</h1>
|
||||
<Link href="/cart/reports/report">
|
||||
@ -74,18 +93,21 @@ export default function Reports() {
|
||||
Добави нов отчет
|
||||
</button>
|
||||
</Link>
|
||||
<label className="mr-4">
|
||||
<input type="radio" name="reportType" value="ServiceReport" defaultChecked />
|
||||
Отчети
|
||||
</label>
|
||||
<label className="mr-4">
|
||||
<input type="radio" name="reportType" value="Experience" />
|
||||
Случка
|
||||
</label>
|
||||
<label className="mr-4">
|
||||
<input type="radio" name="reportType" value="Feedback" />
|
||||
Отзиви
|
||||
</label>
|
||||
<div className="flex gap-2 mb-4">
|
||||
|
||||
<label className={`cursor-pointer px-4 py-2 rounded-full ${filterType === 'ServiceReport' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
<input type="radio" name="reportType" value="ServiceReport" checked={filterType === 'ServiceReport'} onChange={handleFilterChange} className="sr-only" />
|
||||
Отчети
|
||||
</label>
|
||||
<label className={`cursor-pointer px-4 py-2 rounded-full ${filterType === 'Experience' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
<input type="radio" name="reportType" value="Experience" checked={filterType === 'Experience'} onChange={handleFilterChange} className="sr-only" />
|
||||
Случка
|
||||
</label>
|
||||
<label className={`cursor-pointer px-4 py-2 rounded-full ${filterType === 'Feedback' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
<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">
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
@ -98,13 +120,13 @@ export default function Reports() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.map((report) => (
|
||||
{filteredReports.map((report) => (
|
||||
<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">{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.experienceInfo === null || report.experienceInfo === "")
|
||||
{(report.type === ReportType.ServiceReport)
|
||||
? (
|
||||
<>
|
||||
<div><strong>Отчет</strong></div>
|
||||
@ -113,9 +135,16 @@ export default function Reports() {
|
||||
Клипове: {report.videoCount} <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 }} />
|
||||
</>
|
||||
) : (
|
||||
|
@ -88,6 +88,7 @@ async function getAvailabilities(userId) {
|
||||
name: true,
|
||||
isActive: true,
|
||||
isFromPreviousAssignment: true,
|
||||
isFromPreviousMonth: true,
|
||||
dayofweek: true,
|
||||
dayOfMonth: true,
|
||||
startTime: true,
|
||||
|
@ -3,35 +3,61 @@ import Layout from "../components/layout";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { url } from 'inspector';
|
||||
import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute";
|
||||
import axiosInstance from '../src/axiosSecure';
|
||||
|
||||
|
||||
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 (
|
||||
<Layout>
|
||||
<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 */}
|
||||
{/* <p className="p-1">
|
||||
{pdfFiles.map((file, index) => (
|
||||
<p className="p-2">
|
||||
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||
Свали: {file.name}
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</p> */}
|
||||
{pdfFiles.map((file, index) => (
|
||||
<ProtectedRoute>
|
||||
<input type="file" onChange={handleFileUpload} className="mb-4" />
|
||||
{files.map((file, index) => (
|
||||
<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'>
|
||||
{file.name}
|
||||
</a>
|
||||
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">
|
||||
изтрий
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</ProtectedRoute>
|
||||
|
||||
// <React.Fragment key={file.name}>
|
||||
// {index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
|
||||
// <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>
|
||||
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
||||
{pdfFiles.map((file, index) => (
|
||||
<> <p className="pt-2">
|
||||
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||
Свали: {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?
|
||||
reports Report[]
|
||||
Message Message[]
|
||||
EventLog EventLog[]
|
||||
}
|
||||
|
||||
model Availability {
|
||||
@ -179,6 +180,7 @@ model Shift {
|
||||
reportId Int? @unique
|
||||
Report Report? @relation(fields: [reportId], references: [id])
|
||||
isPublished Boolean @default(false) //NEW v1.0.1
|
||||
EventLog EventLog[]
|
||||
|
||||
@@map("Shift")
|
||||
}
|
||||
@ -256,6 +258,23 @@ model Message {
|
||||
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
|
||||
model User {
|
||||
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",
|
||||
"lang": "en-US",
|
||||
"name": "Специално Свидетелстване София",
|
||||
"short_name": "ССС",
|
||||
"short_name": "ССОМ",
|
||||
"start_url": "/",
|
||||
"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.DATABASE_URL = ", process.env.DATABASE_URL);
|
||||
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');
|
||||
|
||||
@ -92,29 +106,13 @@ nextApp
|
||||
// Add the middleware to set 'x-forwarded-host' header
|
||||
server.use((req, res, next) => {
|
||||
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();
|
||||
});
|
||||
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("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
|
||||
|
||||
server.get("/last_schedule_json", (req, res) => {
|
||||
// var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
|
||||
// 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"));
|
||||
});
|
||||
|
||||
|
@ -6,18 +6,11 @@ const levenshtein = require('fastest-levenshtein');
|
||||
|
||||
const fs = typeof window === 'undefined' ? require('fs') : undefined;
|
||||
const path = typeof window === 'undefined' ? require('path') : undefined;
|
||||
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { PrismaClient, UserRole } = require('@prisma/client');
|
||||
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
||||
|
||||
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 { UserRole } = require("@prisma/client");
|
||||
// const { set } = require('date-fns');
|
||||
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info', // Set the default log level
|
||||
@ -249,6 +242,8 @@ exports.getDateFromWeekNrAndDayOfWeek = function (firstMonday, weekNr, dayOfWeek
|
||||
}
|
||||
|
||||
exports.getMonthDatesInfo = function (date) {
|
||||
// cast to date if not daate
|
||||
date = new Date(date);
|
||||
// get first day of the month
|
||||
var firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
// get first day of next month
|
||||
@ -327,7 +322,23 @@ exports.getWeekNumber = function (date) {
|
||||
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) {
|
||||
start = new Date(start);
|
||||
@ -346,6 +357,12 @@ exports.getDateFormated = function (date) {
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
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 path = require('path');
|
||||
|
||||
@ -255,5 +522,6 @@ module.exports = {
|
||||
findPublisher,
|
||||
findPublisherAvailability,
|
||||
runSqlFile,
|
||||
getAvailabilities
|
||||
getAvailabilities,
|
||||
filterPublishersNew
|
||||
};
|
@ -13,45 +13,32 @@ const Handlebars = require('handlebars');
|
||||
|
||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||
const { env } = require("../../next.config");
|
||||
const SMTPTransport = require("nodemailer/lib/smtp-transport");
|
||||
|
||||
// const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
||||
// const SENDER_EMAIL = "sofia@mwitnessing.com";
|
||||
// const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL };
|
||||
// const client = new MailtrapClient({ token: TOKEN });
|
||||
var transporter;
|
||||
if (process.env.EMAIL_SERVICE.toLowerCase() === "mailtrap") {
|
||||
|
||||
let mailtrapTestClient = null;
|
||||
// const mailtrapTestClient = new MailtrapClient({
|
||||
// username: '8ec69527ff2104',//not working now
|
||||
// password: 'c7bc05f171c96c'
|
||||
// });
|
||||
transporter = nodemailer.createTransport({
|
||||
host: process.env.MAILTRAP_HOST || "sandbox.smtp.mailtrap.io",
|
||||
port: process.env.MAILTRAP_PORT || 2525,
|
||||
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 ------------------
|
||||
var lastResult = null;
|
||||
function setResult(result) {
|
||||
@ -87,8 +74,14 @@ function normalizeEmailAddresses(to) {
|
||||
|
||||
|
||||
exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
||||
let sender = '"Специално Свидетелстване София - тест" <demo@mwitnessing.com>';
|
||||
const emailAddresses = normalizeEmailAddresses(to)
|
||||
let sender = process.env.EMAIL_SENDER || '"Специално Свидетелстване София" <sofia@mwitnessing.com>';
|
||||
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 = {
|
||||
from: sender,
|
||||
@ -99,22 +92,11 @@ exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
||||
attachments
|
||||
};
|
||||
|
||||
if (mailtrapTestClient !== null) {
|
||||
// Assuming mailtrapTestClient is correctly set up to send emails
|
||||
await mailtrapTestClient
|
||||
.send(message)
|
||||
.then(console.log)
|
||||
.catch(console.error);
|
||||
|
||||
} else {
|
||||
|
||||
let result = await transporter
|
||||
.sendMail(message)
|
||||
.then(console.log)
|
||||
.catch(console.error);
|
||||
return result;
|
||||
}
|
||||
|
||||
let result = await transporter
|
||||
.sendMail(message)
|
||||
.then(console.log)
|
||||
.catch(console.error);
|
||||
return result;
|
||||
};
|
||||
|
||||
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 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;
|
||||
|
||||
// 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()],
|
||||
// text:
|
||||
// "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
||||
// "Здравей, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
||||
// "Ти регистриран да получавате известия за нови смени на количка.\n" +
|
||||
// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` +
|
||||
// ` ${shftStr} \n\n\n` +
|
||||
|
@ -1,11 +1,11 @@
|
||||
{{!--Subject: ССС: Нужен е заместник --}}
|
||||
{{!--Subject: ССОМ: Нужен е заместник --}}
|
||||
|
||||
<section>
|
||||
<h3>Търси се зместник:
|
||||
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
|
||||
</h3>
|
||||
<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>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
||||
|
@ -1,8 +1,8 @@
|
||||
{{!-- Subject: ССС: Промени в твоята смяна --}}
|
||||
{{!-- Subject: ССОМ: Промени в твоята смяна --}}
|
||||
|
||||
<section>
|
||||
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
|
||||
<p>Здравейте {{firstName}}, </p>
|
||||
<p>Здравей {{firstName}}, </p>
|
||||
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
|
||||
<p>Новаия списък с участници за тази смяна е:</p>
|
||||
<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 version. --}}
|
||||
|
||||
<section>
|
||||
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
|
||||
<p>Здравейте,</p>
|
||||
<p>Здравей,</p>
|
||||
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
|
||||
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
||||
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0">
|
||||
<title>ССС известия</title>
|
||||
<title>ССОМ известия</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -18,7 +18,7 @@
|
||||
</main>
|
||||
|
||||
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
||||
© 2024 ССС. All rights reserved.
|
||||
© 2024 ССОМ. Openly licensed.
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{!-- Subject: ССС: Нови назначени смени--}}
|
||||
{{!-- Subject: ССОМ: Нови назначени смени--}}
|
||||
|
||||
<section>
|
||||
<h2>Здравейте, {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||
<h2>Здравей {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
||||
<p>За месец {{month}} имате следните смени:</p>
|
||||
<div>
|
||||
|
@ -267,3 +267,10 @@ iframe {
|
||||
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",
|
||||
"components/location/LocationForm.js",
|
||||
"pages/cart/locations/[id].tsx.old",
|
||||
"components/publisher/ShiftsList.js"
|
||||
"components/publisher/ShiftsList.js",
|
||||
"src/helpers/data.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
@ -2,6 +2,19 @@
|
||||
|
||||
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) {
|
||||
console.log('Push message', event)
|
||||
if (!(self.Notification && self.Notification.permission === 'granted')) {
|
||||
|
Reference in New Issue
Block a user