This commit is contained in:
Dobromir Popov
2024-04-04 00:03:18 +03:00
72 changed files with 677 additions and 385 deletions

10
.env
View File

@ -1,11 +1,7 @@
#NODE_TLS_REJECT_UNAUTHORIZED='0' #NODE_TLS_REJECT_UNAUTHORIZED='0'
SSL_ENABLED=false # HOST=localhost
NEXT_PUBLIC_PROTOCOL=https # PORT=3003
NEXT_PUBLIC_HOST=localhost # NEXT_PUBLIC_PUBLIC_URL=http://localhost:3003
NEXT_PUBLIC_PORT=3003
NEXTAUTH_URL=https://localhost:3003
# NEXTAUTH_URL_INTERNAL=http://127.0.0.1:3003
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58

View File

@ -1,9 +0,0 @@
NEXT_PUBLIC_PROTOCOL=https
NEXT_PUBLIC_PORT=
NEXT_PUBLIC_HOST=staging.mwhitnessing.com
NEXTAUTH_URL= https://staging.mwhitnessing.com
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
# ? do we need to duplicate this? already defined in the deoployment yml file
DATABASE_URL=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo

View File

@ -1,15 +1,12 @@
NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_TLS_REJECT_UNAUTHORIZED=0
# NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert # NODE_EXTRA_CA_CERTS=C:\\Users\\popov\\AppData\\Local\\mkcert
PROTOCOL=https
PORT=3003
HOST=localhost
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
NEXT_PUBLIC_PROTOCOL=https
NEXT_PUBLIC_HOST=localhost
NEXT_PUBLIC_PORT=3003
NEXTAUTH_URL=https://localhost:3003
SSL_ENABLED=true
TELEGRAM_BOT=true
SSL_KEY=./certificates/localhost-key.pem SSL_KEY=./certificates/localhost-key.pem
SSL_CERT=./certificates/localhost.pem SSL_CERT=./certificates/localhost.pem
DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev DATABASE_URL=mysql://root:Zelen0ku4e@192.168.0.10:3306/cart_dev
# DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart # DATABASE_URL=mysql://cart:cartpw@localhost:3306/cart

View File

@ -1,37 +0,0 @@
NODE_TLS_REJECT_UNAUTHORIZED='0'
# DATABASE_URL="file:./src/data/dev.db"
# DATABASE_URL="mysql://root:Zelen0ku4e@192.168.0.10:3306/cart"
NEXT_PUBLIC_PORT=
# NEXT_PUBLIC_NEXTAUTH_URL=https://cart.d-popov.com
NEXT_PUBLIC_PROTOCOL=https
NEXT_PUBLIC_HOST=cart.d-popov.com
NEXTAUTH_URL=https://cart.d-popov.com
# NEXTAUTH_URL= https://demo.mwhitnessing.com
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58
DATABASE_URL=mysql://cart:cart2023@192.168.0.10:3306/cart_demo
APPLE_ID=
APPLE_TEAM_ID=
APPLE_PRIVATE_KEY=
APPLE_KEY_ID=
AUTH0_ID=Aa9f3HJowauUrmBVY4iQzQJ7fYsaZDbK
AUTH0_SECRET=_c0O9GkyRXkoWMQW7jNExnl6UoXN6O4oD3mg7NZ_uHVeAinCUtcTAkeQmcKXpZ4x
AUTH0_ISSUER=https://dev-wkzi658ckibr1amv.us.auth0.com
FACEBOOK_ID=
FACEBOOK_SECRET=
GITHUB_ID=
GITHUB_SECRET=
# GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
# GOOGLE_SECRET=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
TWITTER_ID=
TWITTER_SECRET=
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
EMAIL_FROM=noreply@example.com

View File

@ -1,7 +1,7 @@
NEXT_PUBLIC_PROTOCOL=https PORT=
NEXT_PUBLIC_PORT= HOST=sofia.mwitnessing.com
NEXT_PUBLIC_HOST=sofia.mwhitnessing.com PROTOCOL=http # we're behind a reverse proxy. SSL is handled by the proxy
NEXTAUTH_URL= https://sofia.mwhitnessing.com NEXT_PUBLIC_PUBLIC_URL= https://sofia.mwitnessing.com
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638

View File

@ -1,13 +1,12 @@
NODE_TLS_REJECT_UNAUTHORIZED='0' PROTOCOL=http
HOST=staging.mwitnessing.com
NEXT_PUBLIC_PORT=5001 PORT=
NEXT_PUBLIC_PROTOCOL=https NEXT_PUBLIC_PUBLIC_URL=https://staging.mwitnessing.com
NEXT_PUBLIC_HOST=cart.d-popov.com
NEXTAUTH_URL=https://cart.d-popov.com
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32 # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
NEXTAUTH_SECRET=ed8a9681efc414df89dfd03cd188ed58 NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
DATABASE_URL=mysql://cart:cartpw@192.168.0.10:3306/cart_dev # ? do we need to duplicate this? already defined in the deoployment yml file
DATABASE_URL=mysql://jwpwsofia_demo:dwxhns9p9vp248@mariadb:3306/jwpwsofia_demo
APPLE_ID= APPLE_ID=
APPLE_TEAM_ID= APPLE_TEAM_ID=
@ -23,14 +22,11 @@ FACEBOOK_SECRET=
GITHUB_ID= GITHUB_ID=
GITHUB_SECRET= GITHUB_SECRET=
GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com # GOOGLE_ID=926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com
GOOGLE_SECRET=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 # GOOGLE_SECRET=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
TWITTER_ID= TWITTER_ID=
TWITTER_SECRET= TWITTER_SECRET=
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525 EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
EMAIL_FROM=noreply@example.com EMAIL_FROM=noreply@example.com
GMAIL_EMAIL_USERNAME=
GMAIL_EMAIL_APP_PASS=

4
.vscode/launch.json vendored
View File

@ -6,7 +6,7 @@
"configurations": [ "configurations": [
{ {
"name": "Run npm nodemon (DEV)", "name": "Run npm nodemon (DEV)",
"command": "npm run debug-env", "command": "npm run debug",
"request": "launch", "request": "launch",
"type": "node-terminal", "type": "node-terminal",
"preLaunchTask": "killInspector", "preLaunchTask": "killInspector",
@ -35,7 +35,7 @@
"request": "launch", "request": "launch",
"type": "node-terminal", "type": "node-terminal",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"command": "conda activate node && npm run debug-env", "command": "conda activate node && npm run debug",
}, },
{ {
"name": "Run conda npm TEST", "name": "Run conda npm TEST",

6
_deploy/appleKey.p8 Normal file
View File

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/
wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ
c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6
/KyhaFLl
-----END PRIVATE KEY-----

View File

@ -0,0 +1 @@
-----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/ wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6 /KyhaFLl -----END PRIVATE KEY-----

View File

@ -1,6 +1,6 @@
version: "3" version: "3"
services: services:
nextjs-app: # https://sofia.mwhitnessing.com/ nextjs-app: # https://sofia.mwitnessing.com/
hostname: jwpw-app-staging # jwpw-nextjs-app-1 hostname: jwpw-app-staging # jwpw-nextjs-app-1
image: docker.d-popov.com/jwpw:latest image: docker.d-popov.com/jwpw:latest
volumes: volumes:

View File

@ -1,6 +1,6 @@
version: "3" version: "3"
services: services:
nextjs-app: # https://sofia.mwhitnessing.com/ nextjs-app: # https://sofia.mwitnessing.com/
hostname: jwpw-app # jwpw-nextjs-app-1 hostname: jwpw-app # jwpw-nextjs-app-1
image: docker.d-popov.com/jwpw:latest image: docker.d-popov.com/jwpw:latest
deploy: deploy:

View File

@ -10,7 +10,7 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then
mkdir /tmp/clone mkdir /tmp/clone
# Clone the repository # Clone the repository
git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwhitnessing.git /tmp/clone || exit 1 git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwitnessing.git /tmp/clone || exit 1
# Synchronize all files except package.json and package-lock.json to /app # Synchronize all files except package.json and package-lock.json to /app
rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files" rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files"

74
_deploy/setupAppleId.mjs Normal file
View File

@ -0,0 +1,74 @@
#!/bin/node
import { SignJWT } from "jose"
import { createPrivateKey } from "crypto"
if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
Creates a JWT from the components found at Apple.
By default, the JWT has a 6 months expiry date.
Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
Usage:
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
APPLE_ID=com.mwhitnessing.sofia
APPLE_TEAM_ID=XC57P9SXDK
APPLE_KEY_ID=TB3V355G5Y
APPLE_KEY=-----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQc93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6/KyhaFLl -----END PRIVATE KEY-----
node setupAppleId.mjs --kid YOUR_KEY_ID --iss YOUR_TEAM_ID --private_key "$(cat key.p8)" --sub YOUR_CLIENT_ID --expires_in 15778800
node setupAppleId.mjs --kid TB3V355G5Y --iss XC57P9SXDK --sub com.mwhitnessing.sofia --private_key "-----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgL3WoWMr7zzqtZdF/ wNEJ9+yMP2qNJV305gTdF+++hLOgCgYIKoZIzj0DAQehRANCAATqlUN+GE7/r8UQ c93hRG9UxCtBcJEcgSGwYVPtZvA5igUBxY/6+RO/Tcnq9xT/6PZD0A82vMNSjoJ6 /KyhaFLl -----END PRIVATE KEY-----"
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")))}`)
}

View File

@ -187,3 +187,12 @@ fix availability repeat checks
sometimes delete from mycalendar fails sometimes delete from mycalendar fails
saturday shifts start at 12:00 / dymamic saturday shifts start at 12:00 / dymamic
-------------------------------
Add availability type UNAVAILABLE/ AWAY (like Estelle, Rick, Me)
why "Александра Чернъшова" seems available every shift thursdays?
fix Time ZONE (currently Z, but it leads to shift when the DST changes ( winter entries are shifter in summer))
защо Марсел Клайнер е червен четв 11 април? - има предпочитания и е в номата
fix repeating availabilities - Tanq kolcjanova only blue first thursday
add assignment in calendar planner
fix database

View File

@ -40,7 +40,7 @@ class ExampleForm extends React.Component {
} }
const [item, set] = useState({ const [item, set] = useState({
isactive: true, isActive: true,
}); });
const router = useRouter(); const router = useRouter();
@ -63,7 +63,7 @@ class ExampleForm extends React.Component {
handleChange = ({ target }) => { handleChange = ({ target }) => {
if (target.name === "isactive") { if (target.name === "isActive") {
set({ ...item, [target.name]: target.checked }); set({ ...item, [target.name]: target.checked });
} else if (target.name === "age") { } else if (target.name === "age") {
set({ ...item, [target.name]: parseInt(target.value) }); set({ ...item, [target.name]: parseInt(target.value) });
@ -100,8 +100,8 @@ class ExampleForm extends React.Component {
<h3>{router.query?.id ? "Редактирай" : "Създай"} Item </h3> <h3>{router.query?.id ? "Редактирай" : "Създай"} Item </h3>
<div className="mb-4"> <div className="mb-4">
<div className="form-check"> <div className="form-check">
<input className="checkbox" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={item.isactive} autoComplete="off" /> <input className="checkbox" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={item.isActive} autoComplete="off" />
<label className="label" htmlFor="isactive"> <label className="label" htmlFor="isActive">
Is Active</label> Is Active</label>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import axiosInstance from '../../src/axiosSecure'; import axiosInstance from '../../src/axiosSecure';
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback, use } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { DatePicker } from '@mui/x-date-pickers/DatePicker';
@ -26,9 +26,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
const [editMode, setEditMode] = useState(existingItems.length > 0); const [editMode, setEditMode] = useState(existingItems.length > 0);
const [publisher, setPublisher] = useState({ id: publisherId }); const [publisher, setPublisher] = useState({ id: publisherId });
const [day, setDay] = useState(new Date(date)); const [day, setDay] = useState(new Date(date));
const [doRepeat, setDoRepeat] = useState(false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const [repeatUntil, setRepeatUntil] = useState(null);
const [canUpdate, setCanUpdate] = useState(true); const [canUpdate, setCanUpdate] = useState(true);
const [timeSlots, setTimeSlots] = useState([]); const [timeSlots, setTimeSlots] = useState([]);
@ -39,13 +36,17 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
dayOfMonth: null, dayOfMonth: null,
// startTime: "08:00", // startTime: "08:00",
// endTime: "20:00", // endTime: "20:00",
isactive: true, isActive: true,
repeatWeekly: false, repeatWeekly: false,
endDate: null, endDate: null,
isFirst: false, isFirst: false,
isLast: false, isLast: false,
}]); }]);
const [doRepeat, setDoRepeat] = useState(existingItems && existingItems.length > 0 ? existingItems[0].repeatWeekly : false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const [repeatUntil, setRepeatUntil] = useState(null);
const [isInline, setInline] = useState(inline || false); const [isInline, setInline] = useState(inline || false);
const [config, setConfig] = useState(null); const [config, setConfig] = useState(null);
useEffect(() => { useEffect(() => {
@ -69,6 +70,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
const response = await axiosInstance.get(`/api/data/availabilities/${id}`); const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
setAvailabilities([response.data]); setAvailabilities([response.data]);
setEditMode(true); setEditMode(true);
setDoRepeat(response.data.repeatWeekly);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error("Error fetching availability data."); toast.error("Error fetching availability data.");
@ -85,27 +87,29 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
e.preventDefault(); e.preventDefault();
try { try {
const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots); const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots);
let avs = availabilities.filter(av => av.type !== "assignment");
// Determine if we need to delete and recreate, or just update // Determine if we need to delete and recreate, or just update
const shouldRecreate = availabilities.length !== groupedTimeSlots.length || availabilities.some(av => !av.id); let shouldRecreate = avs.length > 0 && avs.length !== groupedTimeSlots.length || avs.some(av => !av.id);
shouldRecreate = shouldRecreate || ( avs.length == 0 && availabilities.length > 0);
//create availability if we open a form with assignment without availability
if (shouldRecreate) { if (shouldRecreate) {
// Delete existing availabilities if they have an ID // Delete existing availabilities if they have an ID
console.log("Recreating availabilities"); console.log("Recreating availabilities");
await Promise.all(availabilities.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`))); await Promise.all(avs.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`)));
// Create new availabilities // Create new availabilities
const createdAvailabilities = await Promise.all(groupedTimeSlots.map(async group => { avs = await Promise.all(groupedTimeSlots.map(async group => {
const newAvailability = createAvailabilityFromGroup(group, publisher.id); const newAvailability = createAvailabilityFromGroup(group, publisher.id);
const response = await axiosInstance.post(urls.apiUrl, newAvailability); const response = await axiosInstance.post(urls.apiUrl, newAvailability);
return response.data; // Assuming the new availability is returned return response.data; // Assuming the new availability is returned
})); }));
setAvailabilities(createdAvailabilities); setAvailabilities(avs);
} else { } else {
// Update existing availabilities // Update existing availabilities
console.log("Updating existing availabilities"); console.log("Updating existing availabilities");
const updatedAvailabilities = await Promise.all(availabilities.map(async (availability, index) => { avs = await Promise.all(avs.map(async (availability, index) => {
const group = groupedTimeSlots[index]; const group = groupedTimeSlots[index];
const id = availability.id; const id = availability.id;
const updatedAvailability = updateAvailabilityFromGroup(availability, group); const updatedAvailability = updateAvailabilityFromGroup(availability, group);
@ -119,7 +123,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
return updatedAvailability; return updatedAvailability;
})); }));
setAvailabilities(updatedAvailabilities); setAvailabilities(avs);
} }
handleCompletion({ updated: true }); handleCompletion({ updated: true });
@ -202,17 +206,32 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport; availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport; availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport;
availability.repeatWeekly = doRepeat; delete availability.weekOfMonth;
availability.dayOfMonth = doRepeat ? null : availability.startTime.getDate(); if (doRepeat) {
availability.endDate = doRepeat ? repeatUntil : null; 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(); availability.dateOfEntry = new Date();
if (availability.parentAvailabilityId) {
availability.parentAvailability = { connect: { id: parentAvailabilityId } };
}
delete availability.parentAvailabilityId;
return availability; return availability;
} }
const handleDelete = async (e) => { const handleDelete = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const deletePromises = availabilities.map(async (availability) => { let avs = availabilities.filter(av => av.type !== "assignment");
const deletePromises = avs.map(async (availability) => {
if (availability.id) { if (availability.id) {
// console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id); // console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id);
await axiosInstance.delete(urls.apiUrl + availability.id); await axiosInstance.delete(urls.apiUrl + availability.id);
@ -288,8 +307,7 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => { const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
const [allDay, setAllDay] = useState(false); const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
const handleAllDayChange = (e) => { const handleAllDayChange = (e) => {
const updatedSlots = slots.map(slot => ({ const updatedSlots = slots.map(slot => ({
...slot, ...slot,
@ -297,7 +315,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
})); }));
setSlots(updatedSlots); setSlots(updatedSlots);
setAllDay(e.target.checked) setAllDay(e.target.checked)
setCanUpdate(true); // setCanUpdate(slots.some(slot => slot.isChecked));
const anyChecked = updatedSlots.some(slot => slot.isChecked);
setCanUpdate(anyChecked);
console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots)); console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots));
}; };
useEffect(() => { useEffect(() => {
@ -352,9 +372,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
return ( return (
<div key={index} className="mb-1 flex justify-between items-center"> <div key={index} className="mb-1 flex justify-between items-center">
<label className={`checkbox-container flex items-center mb-2 ${allDay ? 'opacity-50' : ''}`}> <label className={`checkbox-container flex items-center mb-2 `}>
<input type="checkbox" checked={slot.isChecked || allDay} onChange={() => handleSlotCheckedChange(slot)} <input type="checkbox" checked={slot.isChecked || allDay} onChange={() => handleSlotCheckedChange(slot)}
disabled={allDay}
className="form-checkbox h-5 w-5 text-gray-600 mx-2" /> className="form-checkbox h-5 w-5 text-gray-600 mx-2" />
{slotLabel} {slotLabel}
<span className="checkmark"></span> <span className="checkmark"></span>
@ -362,11 +382,11 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
{/* Conditionally render transport checkbox based on slot being first or last */} {/* Conditionally render transport checkbox based on slot being first or last */}
{slot.transportNeeded && ( {slot.transportNeeded && (
<label className={`checkbox-container flex items-center ${(!slot.isChecked || allDay) ? 'opacity-50' : ''}`}> <label className={`checkbox-container flex items-center ${(!slot.isChecked) ? 'opacity-50' : ''}`}>
<input type="checkbox" <input type="checkbox"
className="form-checkbox h-5 w-5 text-gray-600 mx-2" className="form-checkbox h-5 w-5 text-gray-600 mx-2"
checked={slot.isWithTransport} checked={slot.isWithTransport}
disabled={!slot.isChecked || allDay} disabled={!slot.isChecked}
onChange={() => handleTransportChange(slot)} /> onChange={() => handleTransportChange(slot)} />
{slot.isFirst ? 'Вземане' : 'Връщане'} {slot.isFirst ? 'Вземане' : 'Връщане'}
<span className="checkmark"></span> <span className="checkmark"></span>
@ -395,12 +415,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
<div className="mb-2"> <div className="mb-2">
<DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} /> <DatePicker label="Изберете дата" value={day} onChange={(value) => setDay({ value })} />
</div> </div>
<div>
<div className="mb-1">
{/* Time slot checkboxes */}
<TimeSlotCheckboxes slots={timeSlots} setSlots={setTimeSlots} items={availabilities} />
</div>
</div>
<div className="mb-2"> <div className="mb-2">
<label className="checkbox-container"> <label className="checkbox-container">
@ -445,6 +459,13 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
<DatePicker label="До" value={repeatUntil} onChange={(value) => setRepeatUntil({ value })} /> <DatePicker label="До" value={repeatUntil} onChange={(value) => setRepeatUntil({ value })} />
</div> </div>
)} )}
<div>
<div className="mb-1">
{/* Time slot checkboxes */}
<TimeSlotCheckboxes slots={timeSlots} setSlots={setTimeSlots} items={availabilities} />
</div>
</div>
</LocalizationProvider> </LocalizationProvider>
<div className="flex justify-between items-center flex-nowrap w-full p-1"> <div className="flex justify-between items-center flex-nowrap w-full p-1">

View File

@ -36,7 +36,7 @@ model Availability {
weekOfMonth Int? weekOfMonth Int?
startTime DateTime startTime DateTime
endTime DateTime endTime DateTime
isactive Boolean @default(true) isActive Boolean @default(true)
type AvailabilityType @default(Weekly) type AvailabilityType @default(Weekly)
isWithTransport Boolean @default(false) isWithTransport Boolean @default(false)
isFromPreviousAssignment Boolean @default(false) isFromPreviousAssignment Boolean @default(false)
@ -69,7 +69,7 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
dayOfMonth: null, dayOfMonth: null,
startTime: "08:00", startTime: "08:00",
endTime: "20:00", endTime: "20:00",
isactive: true, isActive: true,
repeatWeekly: false, repeatWeekly: false,
endDate: null, endDate: null,
}); });
@ -165,7 +165,6 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
e.preventDefault(); e.preventDefault();
try { try {
if (!availability.name) { if (!availability.name) {
// availability.name = "От календара"; // availability.name = "От календара";
availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime); availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
@ -174,12 +173,13 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
availability.dayofweek = common.getDayOfWeekNameEnEnum(availability.startTime); availability.dayofweek = common.getDayOfWeekNameEnEnum(availability.startTime);
if (availability.repeatWeekly) { if (availability.repeatWeekly) {
availability.dayOfMonth = null; availability.dayOfMonth = null;
availability.weekOfMonth = 0; //weekly recurrance - no need for week of month. special value 0
} else { } else {
availability.endDate = null; availability.endDate = null;
availability.dayOfMonth = availability.startTime.getDate(); availability.dayOfMonth = availability.startTime.getDate();
} }
delete availability.date; //remove date from availability as it is not part of the db model delete availability.date; //remove date from availability as it is not part of the db model. we store the info in startDate and endDate
// ---------------------- CB UI -------------- // ---------------------- CB UI --------------
if (config.checkboxUI.enabled) { if (config.checkboxUI.enabled) {
const selectedSlots = timeSlots.filter(slot => slot.isChecked); const selectedSlots = timeSlots.filter(slot => slot.isChecked);
@ -554,11 +554,11 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on
<div className="mb-2 hidden"> <div className="mb-2 hidden">
<div className="form-check"> <div className="form-check">
<input className="checkbox form-input" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={availability.isactive} autoComplete="off" /> <input className="checkbox form-input" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={availability.isActive} autoComplete="off" />
<label className="label" htmlFor="isactive">активно</label> <label className="label" htmlFor="isActive">активно</label>
</div> </div>
</div> </div>
{/* <input type="hidden" name="isactive" value={availability.isactive} /> */} {/* <input type="hidden" name="isActive" value={availability.isActive} /> */}
<div className="panel-actions"> <div className="panel-actions">
<button className="action-button" onClick={() => handleCompletion()}> Отмени </button> <button className="action-button" onClick={() => handleCompletion()}> Отмени </button>

View File

@ -56,7 +56,7 @@ export default function AvailabilityList({ publisher, showNew }) {
</thead> </thead>
<tbody> <tbody>
{items?.sort((a, b) => new Date(a.startTime) - new Date(b.startTime)).map(item => ( {items?.sort((a, b) => new Date(a.startTime) - new Date(b.startTime)).map(item => (
<tr key={item.id} availability={item} disabled={!item.isactive} className={`${item.isFromPreviousMonth ? 'bg-yellow-200' : ''} ${!item.isactive ? 'opacity-50' : ''}`}> <tr key={item.id} availability={item} disabled={!item.isActive} className={`${item.isFromPreviousMonth ? 'bg-yellow-200' : ''} ${!item.isActive ? 'opacity-50' : ''}`}>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
{item.dayOfMonth ? `${common.getDateFormated(new Date(item.startTime))}` : `Всеки(Всяка) ${common.getDayOfWeekName(new Date(item.startTime))}`} {item.dayOfMonth ? `${common.getDateFormated(new Date(item.startTime))}` : `Всеки(Всяка) ${common.getDayOfWeekName(new Date(item.startTime))}`}
{/* {common.getDateFormated(new Date(item.startTime))} */} {/* {common.getDateFormated(new Date(item.startTime))} */}

View File

@ -70,7 +70,7 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
const newAssignment = { const newAssignment = {
publisher: { connect: { id: publisher.id } }, publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } }, shift: { connect: { id: shiftId } },
//isactive: true, //isActive: true,
isConfirmed: true, isConfirmed: true,
}; };
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment); const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);

View File

@ -56,7 +56,14 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
// Update internal state when `events` prop changes // Update internal state when `events` prop changes
useEffect(() => { useEffect(() => {
const updatedEvents = events.map(event => ({ //if we have isBySystem - set type to assignment
let updatedEvents = events.map(event => {
if (event.isBySystem) {
event.type = "assignment";
}
return event;
});
updatedEvents = events.map(event => ({
...event, ...event,
date: new Date(event.startTime).setHours(0, 0, 0, 0), date: new Date(event.startTime).setHours(0, 0, 0, 0),
startTime: new Date(event.startTime), startTime: new Date(event.startTime),
@ -206,7 +213,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
// startTime: start, // startTime: start,
// endTime: end, // endTime: end,
// dayOfMonth: start.getDate(), // dayOfMonth: start.getDate(),
// isactive: true, // isActive: true,
// publisherId: publisherId, // publisherId: publisherId,
// // Add any other initial values needed // // Add any other initial values needed
// //set dayOfMonth to null, so that we repeat the availability every week // //set dayOfMonth to null, so that we repeat the availability every week
@ -273,9 +280,14 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
//if event.type is availability show in blue. if it is schedule - green if confirmed, yellow if not confirmed //if event.type is availability show in blue. if it is schedule - green if confirmed, yellow if not confirmed
//if event is not active - show in gray //if event is not active - show in gray
let bgColorClass = 'bg-gray-500'; // Default color for inactive events let bgColorClass = 'bg-gray-500'; // Default color for inactive events
var bgColor = event.isactive ? "" : "bg-gray-500"; var bgColor = event.isActive ? "" : "bg-gray-500";
//ToDo: fix this. maybe we're missing some properties
// if (event.isFromPreviousMonth) {
// // set opacity to 0.5
// bgColor = "bg-orange-500";
// }
if (event.type === "assignment") { if (event.type === "assignment") {
bgColor = event.isTentative ? "bg-red-500" : (event.isConfirmed ? "bg-green-500" : "bg-yellow-500"); bgColor = event.isBySystem ? "bg-red-500" : (event.isConfirmed ? "bg-green-500" : "bg-yellow-500");
//event.title = event.publisher.name; //ToDo: add other publishers names //event.title = event.publisher.name; //ToDo: add other publishers names
//event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime); //event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime);
@ -297,11 +309,14 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
} }
} }
eventStyle = { eventStyle = {
...style, ...style,
// backgroundColor: bgColorClass, // backgroundColor: bgColorClass,
//height: "50px", //height: "50px",
//color: 'white', //color: 'white',
//if (event.isFromPreviousAssignment) { set opacity to 0.5 }
// opacity: event.isFromPreviousMonth ? 0.5 : 1,
whiteSpace: 'normal', // Allow the text to wrap to the next line whiteSpace: 'normal', // Allow the text to wrap to the next line
overflow: 'hidden', // Hide overflowed content overflow: 'hidden', // Hide overflowed content
textOverflow: 'ellipsis' // Add ellipsis to text that's too long to fit textOverflow: 'ellipsis' // Add ellipsis to text that's too long to fit
@ -319,7 +334,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
toast.info("Потвърдено!", { autoClose: 2000 }); toast.info("Потвърдено!", { autoClose: 2000 });
// Update the event data // Update the event data
event.isConfirmed = true; event.isConfirmed = true;
event.isTentative = false; event.isBySystem = false;
// Update the events array by first removing the old event and then adding the updated one // Update the events array by first removing the old event and then adding the updated one
setEvents(currentEvents => { setEvents(currentEvents => {
const filteredEvents = currentEvents.filter(e => e.id !== event.id); const filteredEvents = currentEvents.filter(e => e.id !== event.id);
@ -328,7 +343,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
//store the updated event in the database //store the updated event in the database
var assignment = { var assignment = {
isConfirmed: true, isConfirmed: true,
isTentative: false isBySystem: false
}; };
axiosInstance.put('/api/data/assignments/' + event.id, assignment) axiosInstance.put('/api/data/assignments/' + event.id, assignment)
.then((response) => { .then((response) => {
@ -388,7 +403,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
// orange-500 from Tailwind CSS // orange-500 from Tailwind CSS
backgroundColor = '#f56565'; backgroundColor = '#f56565';
} }
if (event.isactive) { if (event.isActive) {
switch (event.type) { switch (event.type) {
case 'assignment': case 'assignment':
backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS

View File

@ -15,7 +15,7 @@ model CartEvent {
shiftDuration Int shiftDuration Int
shifts Shift[] shifts Shift[]
dayofweek DayOfWeek dayofweek DayOfWeek
isactive Boolean @default(true) isActive Boolean @default(true)
}*/ }*/
interface Location { interface Location {
id: number; id: number;
@ -174,8 +174,8 @@ export default function CartEventForm(props: IProps) {
<div className="mb-4"> <div className="mb-4">
<div className="form-check"> <div className="form-check">
<input className="checkbox" type="checkbox" name="isactive" id="isactive" checked={evt.isactive} onChange={handleChange} /> <input className="checkbox" type="checkbox" name="isActive" id="isActive" checked={evt.isActive} onChange={handleChange} />
<label className='label align-text-bottom' htmlFor="isactive">Active</label> <label className='label align-text-bottom' htmlFor="isActive">Active</label>
</div> </div>
</div> </div>
<div className="panel-actions"> <div className="panel-actions">

View File

@ -28,11 +28,11 @@ export default function LocationCard({ location }) {
<> <>
<div <div
id={`location-card-${location.id}`} id={`location-card-${location.id}`}
className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3 cursor-pointer ${location.isactive ? 'text-gray-900 dark:text-white font-bold' : 'text-gray-400 dark:text-gray-600'}`} className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3 cursor-pointer ${location.isActive ? 'text-gray-900 dark:text-white font-bold' : 'text-gray-400 dark:text-gray-600'}`}
onClick={() => router.push(`/cart/locations/edit/${location.id}`)} onClick={() => router.push(`/cart/locations/edit/${location.id}`)}
> >
<h5 className={`mb-2 text-2xl tracking-tight`}> <h5 className={`mb-2 text-2xl tracking-tight`}>
{location.name} ({location.isactive ? "active" : "inactive"}) {location.name} ({location.isActive ? "active" : "inactive"})
</h5> </h5>
<p className="font-normal text-gray-700 dark:text-gray-200"> <p className="font-normal text-gray-700 dark:text-gray-200">
{location.address} {location.address}

View File

@ -19,7 +19,7 @@ const common = require('src/helpers/common');
// id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
// name String // name String
// address String // address String
// isactive Boolean @default(true) // isActive Boolean @default(true)
// content String? @db.Text // content String? @db.Text
// cartEvents CartEvent[] // cartEvents CartEvent[]
// reports Report[] // reports Report[]
@ -65,7 +65,7 @@ export default function LocationForm() {
const [location, set] = useState({ const [location, set] = useState({
name: "", name: "",
address: "", address: "",
isactive: true, isActive: true,
}); });
// const [isEdit, setIsEdit] = useState(false); // const [isEdit, setIsEdit] = useState(false);
@ -171,11 +171,11 @@ export default function LocationForm() {
<input className="textbox" <input className="textbox"
placeholder="address" id="address" name="address" onChange={handleChange} value={location.address} autoComplete="off" /> placeholder="address" id="address" name="address" onChange={handleChange} value={location.address} autoComplete="off" />
</div> </div>
{/* UI for Location.isactive */} {/* UI for Location.isActive */}
<div className="mb-4"> <div className="mb-4">
<div className="form-check"> <div className="form-check">
<input className="checkbox form-input" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={location.isactive} autoComplete="off" /> <input className="checkbox form-input" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={location.isActive} autoComplete="off" />
<label className="label" htmlFor="isactive">Активна</label> <label className="label" htmlFor="isActive">Активна</label>
</div> </div>
</div> </div>
{/* backupLocation */} {/* backupLocation */}

View File

@ -57,7 +57,7 @@ export default function PublisherCard({ publisher }) {
return isCardVisible ? ( return isCardVisible ? (
// className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3" // className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
<div id={`publisher-card-${publisher.id}`} className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3 <div id={`publisher-card-${publisher.id}`} className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3
${!publisher.isactive ? "opacity-50 bg-gray-200 border-gray-300 text-gray-400" : (publisher.isImported ? "bg-orange-50" : (publisher.isTrained ? "bg-white" : "bg-red-50"))}`} ${!publisher.isActive ? "opacity-50 bg-gray-200 border-gray-300 text-gray-400" : (publisher.isImported ? "bg-orange-50" : (publisher.isTrained ? "bg-white" : "bg-red-50"))}`}
> >
<a <a
href={`/cart/publishers/edit/${publisher.id}`} href={`/cart/publishers/edit/${publisher.id}`}
@ -66,7 +66,7 @@ export default function PublisherCard({ publisher }) {
> >
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"> <h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{publisher.firstName} {publisher.lastName} ({publisher.isactive ? "active" : "inactive"}) {publisher.firstName} {publisher.lastName} ({publisher.isActive ? "active" : "inactive"})
</h5> </h5>
<div className="font-normal text-gray-700 dark:text-gray-200"> <div className="font-normal text-gray-700 dark:text-gray-200">
<p> {publisher.assignments.length} смени общо</p> <p> {publisher.assignments.length} смени общо</p>

View File

@ -23,7 +23,7 @@ import { UserRole } from "@prisma/client";
// lastName String // lastName String
// email String @unique // email String @unique
// phone String? // phone String?
// isactive Boolean @default(true) // isActive Boolean @default(true)
// isImported Boolean @default(false) // isImported Boolean @default(false)
// age Int? // age Int?
// availabilities Availability[] // availabilities Availability[]
@ -75,7 +75,7 @@ export default function PublisherForm({ item, me }) {
}, []); }, []);
const [publisher, set] = useState(item || { const [publisher, set] = useState(item || {
isactive: true, isActive: true,
}); });
const handleChange = ({ target }) => { const handleChange = ({ target }) => {
@ -259,8 +259,8 @@ export default function PublisherForm({ item, me }) {
</div> </div>
<div className="mb-4"> <div className="mb-4">
<div className="form-check"> <div className="form-check">
<input className="checkbox" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={publisher.isactive} autoComplete="off" /> <input className="checkbox" type="checkbox" id="isActive" name="isActive" onChange={handleChange} checked={publisher.isActive} autoComplete="off" />
<label className="label" htmlFor="isactive">Активен</label> <label className="label" htmlFor="isActive">Активен</label>
<input className="checkbox" type="checkbox" id="isTrained" name="isTrained" onChange={handleChange} checked={publisher.isTrained} autoComplete="off" /> <input className="checkbox" type="checkbox" id="isTrained" name="isTrained" onChange={handleChange} checked={publisher.isTrained} autoComplete="off" />
<label className="label" htmlFor="isTrained">Получил обучение</label> <label className="label" htmlFor="isTrained">Получил обучение</label>
<input className="checkbox disabled" type="checkbox" id="isImported" name="isImported" onChange={handleChange} checked={publisher.isImported} autoComplete="off" /> <input className="checkbox disabled" type="checkbox" id="isImported" name="isImported" onChange={handleChange} checked={publisher.isImported} autoComplete="off" />
@ -282,7 +282,7 @@ export default function PublisherForm({ item, me }) {
</select> </select>
</div> </div>
<a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank"> <a href="https://t.me/mwHitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" /> <img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
<span className="align-middle">Телеграм</span> <span className="align-middle">Телеграм</span>
</a> </a>

View File

@ -18,7 +18,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS
const fetchPublishers = async () => { const fetchPublishers = async () => {
console.log("fetchPublishers called"); console.log("fetchPublishers called");
try { try {
let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isactive&searchText=${searchText}&availabilities=false`; let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isActive&searchText=${searchText}&availabilities=false`;
if (filterDate) { if (filterDate) {
url += `&filterDate=${common.getISODateOnly(filterDate)}`; url += `&filterDate=${common.getISODateOnly(filterDate)}`;
@ -29,7 +29,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS
const { data: publishersData } = await axiosInstance.get(url); const { data: publishersData } = await axiosInstance.get(url);
//setPublishers(publishersData); //setPublishers(publishersData);
const activePublishers = publishersData.filter(publisher => publisher.isactive === true); const activePublishers = publishersData.filter(publisher => publisher.isActive === true);
setPublishers(activePublishers); setPublishers(activePublishers);
} catch (error) { } catch (error) {

View File

@ -35,7 +35,7 @@ const ShiftsList = ({ assignments, selectedtab }: ShiftsListProps) => {
try { try {
var assignment = (await axiosInstance.get("/api/data/assignments/" + id)).data; var assignment = (await axiosInstance.get("/api/data/assignments/" + id)).data;
assignment.isConfirmed = false; assignment.isConfirmed = false;
assignment.isTentative = true; // ! assignment.isTentative = true;
// assignment.isDeleted = true; // assignment.isDeleted = true;
await axiosInstance.put("/api/data/assignments/" + id, assignment); await axiosInstance.put("/api/data/assignments/" + id, assignment);
toast.success("Shift Tentative", { toast.success("Shift Tentative", {

View File

@ -99,7 +99,7 @@ export default function Sidebar({ isSidebarOpen, toggleSidebar }) {
try { try {
const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed const response = await axiosInstance.get('/api/data/locations'); // Adjust the API endpoint as needed
const locationsData = response.data const locationsData = response.data
.filter(location => location.isactive === true) .filter(location => location.isActive === true)
.map(location => ({ .map(location => ({
text: location.name, text: location.name,
url: `/cart/locations/${location.id}`, url: `/cart/locations/${location.id}`,

View File

@ -12,7 +12,7 @@ module.exports = {
pageExtensions: ['ts', 'tsx', 'md', 'mdx'], // Replace `jsx?` with `tsx?` pageExtensions: ['ts', 'tsx', 'md', 'mdx'], // Replace `jsx?` with `tsx?`
env: { env: {
env: process.env.NODE_ENV, env: process.env.NODE_ENV,
server: 'http://' + process.env.NEXT_PUBLIC_HOST + ':' + process.env.NEXT_PUBLIC_PORT + '', server: process.env.NEXT_PUBLIC_PUBLIC_URL
}, },
webpack(config, { isServer }) { webpack(config, { isServer }) {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pwwa", "name": "pwwa",
"version": "1.0.1", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pwwa", "name": "pwwa",
"version": "1.0.1", "version": "1.1.1",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^1.4.0", "@auth/prisma-adapter": "^1.4.0",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "pwwa", "name": "pwwa",
"version": "1.0.1", "version": "1.1.2",
"private": true, "private": true,
"description": "JW PW Web App", "description": "JW PW Web App",
"repository": "http://git.d-popov.com/popov/next-cart-app.git", "repository": "http://git.d-popov.com/popov/next-cart-app.git",
@ -10,7 +10,7 @@
"homepage": "https://git.d-popov.com/popov/next-cart-app", "homepage": "https://git.d-popov.com/popov/next-cart-app",
"scripts": { "scripts": {
"debug": "nodemon --inspect server.js", "debug": "nodemon --inspect server.js",
"debug-env": "cross-env NODE_ENV=development dotenv -e .env.development -- nodemon --inspect server.js", "debug-env-dev": "dotenv -e .env.development -- nodemon --inspect server.js",
"build": "next build", "build": "next build",
"buildWin": "npm run build", "buildWin": "npm run build",
"start": "next start", "start": "next start",

View File

@ -35,7 +35,7 @@ export default function App({
rel="stylesheet" rel="stylesheet"
/> />
</Head> </Head>
<SessionProvider session={session}> <SessionProvider session={session} >
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<Component {...pageProps} /> <Component {...pageProps} />
</LocalizationProvider> </LocalizationProvider>

View File

@ -24,12 +24,12 @@ import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshT
export const authOptions: NextAuthOptions = { export const authOptions: NextAuthOptions = {
// https://next-auth.js.org/configuration/providers/oauth // https://next-auth.js.org/configuration/providers/oauth
site: process.env.NEXTAUTH_URL, site: process.env.NEXT_PUBLIC_PUBLIC_URL,
secret: process.env.NEXTAUTH_SECRET, // Ensure you have this set in your .env file secret: process.env.NEXTAUTH_SECRET, // Ensure you have this set in your .env file
//adapter: PrismaAdapter(prisma), //adapter: PrismaAdapter(prisma),
providers: [ providers: [
// register new URL at https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 // register new URL at https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
//Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwhitnessing.com/ //Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwitnessingmwitnessing.com/
GoogleProvider({ GoogleProvider({
clientId: process.env.GOOGLE_ID, clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET, clientSecret: process.env.GOOGLE_SECRET,

View File

@ -1,12 +1,13 @@
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { DayOfWeek } from '@prisma/client'; import { DayOfWeek, AvailabilityType } from '@prisma/client';
const common = require('../../src/helpers/common'); const common = require('../../src/helpers/common');
const data = require('../../src/helpers/data'); const data = require('../../src/helpers/data');
const subq = require('../../prisma/bl/subqueries'); const subq = require('../../prisma/bl/subqueries');
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { all } from "axios";
/** /**
* *
@ -29,7 +30,7 @@ export default async function handler(req, res) {
var action = req.query.action; var action = req.query.action;
var filter = req.query.filter; var filter = req.query.filter;
let date: Date; let date: Date, monthInfo: any;
if (req.query.date) { if (req.query.date) {
date = new Date(req.query.date); date = new Date(req.query.date);
//date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead) //date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead)
@ -77,7 +78,7 @@ export default async function handler(req, res) {
//gets publisher by names with availabilities and assignments //gets publisher by names with availabilities and assignments
case "deleteAvailabilityForPublisher": case "deleteAvailabilityForPublisher":
let publisherId = req.query.publisherId; let publisherId = req.query.publisherId;
let dateFor, monthInfo; let dateFor;
if (req.query.date) { if (req.query.date) {
dateFor = new Date(req.query.date); dateFor = new Date(req.query.date);
//get month info from date //get month info from date
@ -143,7 +144,7 @@ export default async function handler(req, res) {
case "getUnassignedPublishers": case "getUnassignedPublishers":
//let monthInfo = common.getMonthDatesInfo(date); //let monthInfo = common.getMonthDatesInfo(date);
let allPubs = await filterPublishers("id,firstName,lastName,email,isactive".split(","), "", date, true, true, false); let allPubs = await filterPublishers("id,firstName,lastName,email,isActive".split(","), "", date, true, true, false);
let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0); let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0);
res.status(200).json(unassignedPubs); res.status(200).json(unassignedPubs);
break; break;
@ -209,14 +210,135 @@ export default async function handler(req, res) {
res.status(200).json(shiftsForDate); res.status(200).json(shiftsForDate);
break; break;
case "copyOldAvailabilities":
//get all publishers that don't have availabilities for the current month
monthInfo = common.getMonthDatesInfo(date);
// await prisma.availability.deleteMany({
// where: {
// startTime: {
// gte: monthInfo.firstMonday,
// },
// isFromPreviousMonth: true
// }
// });
let outdatedPubs = await prisma.publisher.findMany({
where: {
availabilities: {
none: {
startTime: {
gte: monthInfo.firstMonday,
}
}
}
},
select: {
id: true,
firstName: true,
lastName: true,
availabilities: true
}
});
outdatedPubs.forEach(async pub => {
// avail.startTime >= monthInfo.firstMonday
//get prev month date:
let prevMonth = new Date(monthInfo.firstMonday);
prevMonth.setMonth(prevMonth.getMonth() - 1);
let prevMonthInfo = common.getMonthDatesInfo(prevMonth);
pub.availabilities = pub.availabilities.filter(avail => avail.startTime > prevMonthInfo.firstMonday);
console.log("" + pub.firstName + " " + pub.lastName + " copying " + pub.availabilities.length + " availabilities from previous months.");
pub.availabilities.forEach(async avail => {
//get the new date based on the day of week and week of month
if (!avail.weekOfMonth) {
avail.weekOfMonth = common.getWeekOfMonth(avail.startTime)
}
let origMonthInfo = common.getMonthDatesInfo(avail.startTime);
let newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, avail.weekOfMonth, avail.dayofweek, avail.startTime);
//ToDo: fix double check. also check if we're in 5th week and the month has 4 weeks
// const availability = await data.findPublisherAvailability(publisher.id, newStart);
// if (availability) {
// return;
// }
let newEnd = new Date(newStart.getTime());
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
let data = {
publisherId: pub.id,
dayOfMonth: null,
dayofweek: avail.dayofweek || common.getDayOfWeekNameEnEnum(avail.startTime),
weekOfMonth: avail.weekofMonth || common.getWeekOfMonth(avail.startTime),
// null for auto generated availabilities
//dateOfEntry: new Date(), //avail.dateOfEntry || avail.startTime,
startTime: newStart,
endTime: newEnd,
type: AvailabilityType.Monthly,
isFromPreviousMonth: true,
name: avail.name || "старо предпочитание",
// parentAvailabilityId: avail.id
parentAvailability: {
connect: {
id: avail.id
}
}
}
await prisma.availability.create({ data: data });
//if month has 5 weeks and the monthInfo has 4 weeks copy the availabilities also from the 1st week to the 5th week
if (monthInfo.nrOfWeeks == 5 && avail.weekOfMonth == 1 && origMonthInfo.nrOfWeeks == 4) {
newStart = common.getDateFromWeekNrAndDayOfWeek(monthInfo.firstMonday, 5, avail.dayofweek, avail.startTime);
newEnd = new Date(newStart.getTime());
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
data.weekOfMonth = 5;
data.startTime = newStart;
data.endTime = newEnd;
await prisma.availability.create({ data: data });
}
});
});
//convert old assignments to availabilities
res.status(200).json({ "message": "ok" });
break;
case "deleteCopiedAvailabilities":
//delete all availabilities that are copied from previous months
monthInfo = common.getMonthDatesInfo(date);
await prisma.availability.deleteMany({
where: {
startTime: {
gte: monthInfo.firstMonday,
},
isFromPreviousMonth: true
}
});
case "replaceInAssignment": case "replaceInAssignment":
const { oldPublisherId, newPublisherId, shiftId } = req.method === "POST" ? req.body : req.query; const { oldPublisherId, newPublisherId, shiftId } = req.method === "POST" ? req.body : req.query;
const prisma = common.getPrismaClient();
const result = await replaceInAssignment(oldPublisherId, newPublisherId, shiftId); const result = await replaceInAssignment(oldPublisherId, newPublisherId, shiftId);
res.status(200).json(result); res.status(200).json(result);
break; break;
case "updateShifts":
//get all shifts for the month and publish them (we pass date )
let monthInfo = common.getMonthDatesInfo(date);
let isPublished = common.parseBool(req.query.isPublished);
let updated = await prisma.shift.updateMany({
where: {
startTime: {
gte: new Date(monthInfo.firstMonday.getFullYear(), monthInfo.firstMonday.getMonth(), 1),
lt: new Date(monthInfo.lastSunday.getFullYear(), monthInfo.lastSunday.getMonth() + 1, 1),
}
},
data: {
isPublished: isPublished
}
});
console.log("Updated shifts: " + updated.count);
res.status(200).json({ "message": "ok" });
break;
default: default:
res.status(200).json({ res.status(200).json({
"message": "no action '" + action + "' found" "message": "no action '" + action + "' found"
@ -442,8 +564,8 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
// also, permanent weekly availabilities will have dayOfMonth = null and type = 0 // also, permanent weekly availabilities will have dayOfMonth = null and type = 0
// for 0 we will match by dayOfWeekEnum and times // for 0 we will match by dayOfWeekEnum and times
// for 1 we will match by exact date and times // for 1 we will match by exact date and times
// for 2 we will match by dayofweek, weeknr and times // for 2 we will match by dayofweek, weekOfMonth and times
// for 3 we will match by dayofweek, weeknr and times - this is the same as 2, but we will not count them as availabilities for the current month // for 3 we will match by dayofweek, weekOfMonth and times - this is the same as 2, but we will not count them as availabilities for the current month
// generaion of schedule: // generaion of schedule:
@ -471,9 +593,9 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
// This includes availabilities from previous assignments but not with preference // This includes availabilities from previous assignments but not with preference
{ {
dayOfMonth: null, dayOfMonth: null, // includes monthly and weekly repeats
dayofweek: dayOfWeekEnum, dayofweek: dayOfWeekEnum
// ToDo: and weekNr // ToDo: and weekOfMonth
//startTime: { gte: currentMonthStart }, //startTime: { gte: currentMonthStart },
} }
] ]
@ -525,7 +647,7 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
// dayOfMonth: true, // dayOfMonth: true,
// startTime: true, // startTime: true,
// endTime: true, // endTime: true,
// weekNr: true, // weekOfMonth: true,
// type: true // type: true
// }, // },
// where: { // where: {
@ -616,6 +738,11 @@ export async function filterPublishers(selectFields, searchText, filterDate, fet
return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd; return avail.dayOfMonth != null && avail.startTime >= currentMonthStart; // && avail.startTime <= currentMonthEnd;
}); });
//if pub has availabilities for the current day
pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => {
return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
});
}); });
if (filterDate && useDateFilter) { if (filterDate && useDateFilter) {
@ -690,7 +817,7 @@ async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
data: { data: {
publisherId: newPublisherId, publisherId: newPublisherId,
isConfirmed: false, isConfirmed: false,
isTentative: true, isBySystem: true,
isMailSent: false isMailSent: false
} }
}); });

View File

@ -89,7 +89,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const shifts = await prisma.shift.findMany({ const shifts = await prisma.shift.findMany({
where: { where: {
isactive: true, isActive: true,
isPublished: true,
startTime: { startTime: {
gte: fromDate, gte: fromDate,
lt: toDate, lt: toDate,

View File

@ -68,7 +68,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
case "test": case "test":
var data = prisma.shift.findMany({ var data = prisma.shift.findMany({
where: { where: {
isactive: true isActive: true
} }
}); });
@ -110,7 +110,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
} }
console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")"); console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")");
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":{"$eq":true}}`); const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":{"$eq":true}}`);
//// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo); //// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo);
//use filterPublishers from /pages/api/data/index.ts to get publishers with stats //use filterPublishers from /pages/api/data/index.ts to get publishers with stats
@ -308,7 +308,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
assignments: 'true', assignments: 'true',
availabilities: 'true', availabilities: 'true',
date: common.getISODateOnly(shiftStart), date: common.getISODateOnly(shiftStart),
select: 'id,firstName,lastName,isactive,desiredShiftsPerMonth' select: 'id,firstName,lastName,isActive,desiredShiftsPerMonth'
}); });
let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data; let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data;
let availablePublishers = allAvailablePublishers; let availablePublishers = allAvailablePublishers;
@ -444,7 +444,7 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont
} }
//create shifts using API //create shifts using API
// const { data: createdShifts } = await axios.post(`${process.env.NEXTAUTH_URL}/api/data/shifts`, shiftsToCreate); // const { data: createdShifts } = await axios.post(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/shifts`, shiftsToCreate);
//const { data: allshifts } = await axios.get(`/api/data/shifts`); //const { data: allshifts } = await axios.get(`/api/data/shifts`);
return {}; //allshifts; return {}; //allshifts;
@ -616,12 +616,12 @@ async function ImportShiftsFromDocx(axios: Axios) {
// prisma.publisher.findMany({ // prisma.publisher.findMany({
// where: { // where: {
// isactive: true, // isActive: true,
// }, // },
// include: { // include: {
// availabilities: { // availabilities: {
// where: { // where: {
// isactive: true, // isActive: true,
// }, // },
// }, // },
// assignments: { // assignments: {

View File

@ -30,7 +30,7 @@ export default function AvPage({ initialItems, id }: IProps) {
date: new Date(item.startTime), date: new Date(item.startTime),
start: new Date(item.startTime), start: new Date(item.startTime),
end: new Date(item.endTime), end: new Date(item.endTime),
isactive: item.isactive, isActive: item.isActive,
publisherId: item.publisher.id, publisherId: item.publisher.id,
dayOfMonth: item.dayOfMonth, dayOfMonth: item.dayOfMonth,
dayOfWeek: item.dayOfWeek, dayOfWeek: item.dayOfWeek,
@ -66,9 +66,9 @@ export default function AvPage({ initialItems, id }: IProps) {
</thead> </thead>
<tbody> <tbody>
{initialItems?.map((item: Availability) => ( {initialItems?.map((item: Availability) => (
<tr key={item.id} className={item.isactive ? "" : "text-gray-300"}> <tr key={item.id} className={item.isActive ? "" : "text-gray-300"}>
<td className="px-6 py-4 whitespace-nowrap "> <td className="px-6 py-4 whitespace-nowrap ">
{item.id} {item.isactive} {item.id} {item.isActive}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
{item.publisher.lastName}, {item.publisher.firstName} {item.publisher.lastName}, {item.publisher.firstName}
@ -133,19 +133,19 @@ export const getServerSideProps = async (context) => {
const role = session?.user.role; const role = session?.user.role;
console.log("server role: " + role); console.log("server role: " + role);
var queryUrl = process.env.NEXTAUTH_URL + "/api/data/availabilities?select=id,name,isactive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id"; var queryUrl = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities?select=id,name,isActive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id";
if (role === UserRole.USER || context.query.my) { if (role === UserRole.USER || context.query.my) {
queryUrl += `&where={"publisherId":"${session?.user.id}"}`; queryUrl += `&where={"publisherId":"${session?.user.id}"}`;
} else if (role == UserRole.ADMIN) { } else if (role == UserRole.ADMIN) {
if (context.query.id) { if (context.query.id) {
queryUrl += `&where={"publisherId":"${context.query.id}"}`; queryUrl += `&where={"publisherId":"${context.query.id}"}`;
} else { } else {
queryUrl += `&where={"isactive":true}`; queryUrl += `&where={"isActive":true}`;
} }
} }
var resp = await axios.get( var resp = await axios.get(
queryUrl queryUrl
// process.env.NEXTAUTH_URL + "/api/data/availabilities?include=publisher", // process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities?include=publisher",
, { decompress: true }); , { decompress: true });
var items = resp.data; var items = resp.data;
console.log("got " + items.length + " availabilities"); console.log("got " + items.length + " availabilities");

View File

@ -31,7 +31,7 @@ export const getServerSideProps = async (context) => {
}; };
} }
const { data: item } = await axios.get( const { data: item } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/availabilities/" + context.params.id process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/availabilities/" + context.params.id
); );
return { return {

View File

@ -57,6 +57,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const [allShifts, setAllShifts] = useState(initialShifts); const [allShifts, setAllShifts] = useState(initialShifts);
const [isPublished, setIsPublished] = useState(() => initialShifts.some(shift => shift.isPublished));
const [value, onChange] = useState<Date>(new Date()); const [value, onChange] = useState<Date>(new Date());
const [shifts, setShifts] = React.useState([]); const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
@ -100,7 +101,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date)); console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
if (isCheckboxChecked) { if (isCheckboxChecked) {
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`); console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`); const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
setAvailablePubs(availablePubsForDate); setAvailablePubs(availablePubsForDate);
} }
else { else {
@ -108,7 +109,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
try { try {
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`); const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
setShifts(shiftsForDate); setShifts(shiftsForDate);
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`); setIsPublished(shiftsForDate.some(shift => shift.isPublished));
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
availablePubsForDate.forEach(pub => { availablePubsForDate.forEach(pub => {
pub.canTransport = pub.availabilities.some(av => pub.canTransport = pub.availabilities.some(av =>
@ -172,8 +174,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
// currentMonthAssignments and previousMonthAssignments properties // currentMonthAssignments and previousMonthAssignments properties
// Sort publishers based on availability and then by assignment counts. // Sort publishers based on availability and then by assignment counts.
const sortedPubs = updatedPubs.sort((a, b) => { const sortedPubs = updatedPubs.sort((a, b) => {
if (a.isactive !== b.isactive) { if (a.isActive !== b.isActive) {
return a.isactive ? -1 : 1; return a.isActive ? -1 : 1;
} }
// First, sort by isselected. // First, sort by isselected.
if (a.isSelected !== b.isSelected) { if (a.isSelected !== b.isSelected) {
@ -355,7 +357,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const newAssignment = { const newAssignment = {
publisher: { connect: { id: publisher.id } }, publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } }, shift: { connect: { id: shiftId } },
isactive: true, isActive: true,
isConfirmed: true isConfirmed: true
}; };
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment); const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
@ -511,10 +513,26 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
} }
} }
const togglePublished = async () => {
try {
const publishState = !isPublished; // Toggle the state
const isPublishedParam = publishState ? 'true' : 'fasle';
await axiosInstance.get(`/api/?action=updateShifts&isPublished=${isPublishedParam}&date=${common.getISODateOnly(value)}`);
setIsPublished(publishState); // Update state based on the action
} catch (error) {
console.log(error);
}
}
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false); const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
async function copyOldAvailabilities(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
}
return ( return (
<> <>
<Layout> <Layout>
@ -550,6 +568,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
}} }}
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?" message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
/> />
<button
className={`button btn m-2 ${isPublished ? 'hover:bg-gray-100 bg-yellow-500' : 'hover:bg-red-300 bg-blue-400'}`}
onClick={togglePublished}>
<i className={`fas ${isPublished ? 'fa-check' : 'fa-close'} mr-2`}></i>
{isPublished ? "Скрий" : "Публикувай"} графика (м.)
</button>
<div className="relative inline-block text-left"> <div className="relative inline-block text-left">
<button <button
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`} className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
@ -591,6 +615,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}> <button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button> {isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button> <button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={copyOldAvailabilities}><i className="fas fa-copy mr-2"></i> Прехвърли предпочитанията</button>
</div> </div>
</div> </div>
)} )}
@ -669,7 +694,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
// Determine border class if selected // Determine border class if selected
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : ''; const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
// Determine opacity class // Determine opacity class
const activeOpacityClass = pub.isactive ? '' : 'opacity-25'; const activeOpacityClass = pub.isActive ? '' : 'opacity-25';
return ( return (
@ -866,9 +891,9 @@ import axiosServer from '../../../src/axiosServer';
import { start } from 'repl'; import { start } from 'repl';
export const getServerSideProps = async (context) => { export const getServerSideProps = async (context) => {
const axios = await axiosServer(context); const axios = await axiosServer(context);
const baseUrl = common.getBaseUrl(); // const baseUrl = common.getBaseUrl();
console.log('runtime BaseUrl: ' + baseUrl); // console.log('runtime BaseUrl: ' + baseUrl);
console.log('runtime NEXTAUTH_URL: ' + process.env.NEXTAUTH_URL); console.log('runtime NEXT_PUBLIC_PUBLIC_URL: ' + process.env.NEXT_PUBLIC_PUBLIC_URL);
console.log('Runtime Axios Base URL:', axios.defaults.baseURL); console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
const currentDate = new Date(); const currentDate = new Date();
@ -878,20 +903,20 @@ export const getServerSideProps = async (context) => {
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`; const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
const prismaClient = common.getPrismaClient(); const prismaClient = common.getPrismaClient();
// let events = await prismaClient.cartEvent.findMany({ where: { isactive: true } }); // let events = await prismaClient.cartEvent.findMany({ where: { isActive: true } });
// events = events.map(event => ({ // events = events.map(event => ({
// ...event, // ...event,
// // Convert Date objects to ISO strings // // Convert Date objects to ISO strings
// startTime: event.startTime.toISOString(), // startTime: event.startTime.toISOString(),
// endTime: event.endTime.toISOString(), // endTime: event.endTime.toISOString(),
// })); // }));
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":true}`); const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":true}`);
//const { data: shifts } = await axios.get(url); //const { data: shifts } = await axios.get(url);
// get all shifts for the month, including assigments // get all shifts for the month, including assigments
let shifts = await prismaClient.shift.findMany({ let shifts = await prismaClient.shift.findMany({
where: { where: {
isactive: true, isActive: true,
startTime: { startTime: {
gte: firstDayOfMonth, gte: firstDayOfMonth,
//lt: lastDayOfMonth //lt: lastDayOfMonth

View File

@ -7,9 +7,9 @@ export const getServerSideProps = async (context) => {
console.log("edit page getServerSideProps"); console.log("edit page getServerSideProps");
const axios = await axiosServer(context); const axios = await axiosServer(context);
const { id } = context.query; const { id } = context.query;
const { data } = await axios.get(`${process.env.NEXTAUTH_URL}/api/data/cartevents/` + id); const { data } = await axios.get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/cartevents/` + id);
const locations = await axios const locations = await axios
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`) .get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations?select=id,name`)
.then((res) => { .then((res) => {
console.log("locations: " + JSON.stringify(res.data)); console.log("locations: " + JSON.stringify(res.data));
return res.data; return res.data;

View File

@ -69,7 +69,7 @@ export default function CartEventPage({ items, locations }: ICartEventPageProps)
{item.shiftDuration} {item.shiftDuration}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
{item.isactive ? "Yes" : "No"} {item.isActive ? "Yes" : "No"}
</td> </td>
<td> <td>
<button className="button bg-blue-500 hover:bg-blue-700" <button className="button bg-blue-500 hover:bg-blue-700"

View File

@ -26,7 +26,7 @@ export const getServerSideProps = async (context) => {
const axios = await axiosServer(context); const axios = await axiosServer(context);
const locations = await axios const locations = await axios
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`) .get(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations?select=id,name`)
.then((res) => { .then((res) => {
console.log("locations: " + JSON.stringify(res.data)); console.log("locations: " + JSON.stringify(res.data));
return res.data; return res.data;
@ -41,7 +41,7 @@ export const getServerSideProps = async (context) => {
const { id } = context.query.id; const { id } = context.query.id;
const { data: item } = await axiosInstance.get( const { data: item } = await axiosInstance.get(
process.env.NEXTAUTH_URL + "/api/data/cartevents/" + context.params.id process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/cartevents/" + context.params.id
); );
return { return {

View File

@ -99,11 +99,11 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
const axios = await axiosServer(context); const axios = await axiosServer(context);
const { data: location } = await axios.get( const { data: location } = await axios.get(
`${process.env.NEXTAUTH_URL}/api/data/locations/${context.params.id}` `${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/locations/${context.params.id}`
); );
if (location.backupLocationId !== null) { if (location.backupLocationId !== null) {
const { data: backupLocation } = await axios.get( const { data: backupLocation } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/locations/" + location.backupLocationId process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/locations/" + location.backupLocationId
); );
location.backupLocationName = backupLocation.name; location.backupLocationName = backupLocation.name;
location.backupLocationContent = backupLocation ? backupLocation.content : ""; location.backupLocationContent = backupLocation ? backupLocation.content : "";

View File

@ -29,7 +29,7 @@ export const getServerSideProps = async (context) => {
}; };
} }
const { data: item } = await axios.get( const { data: item } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/locations/" + context.params.id process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/locations/" + context.params.id
); );
console.log(item) //this is the location object console.log(item) //this is the location object
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");

View File

@ -32,7 +32,7 @@ export const getServerSideProps = async (context) => {
} }
const { data: loc } = await axios.get( const { data: loc } = await axios.get(
`${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id
); );
console.log(location) //this is the location object console.log(location) //this is the location object
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");

View File

@ -44,7 +44,7 @@ export const getServerSideProps = async (context) => {
props: {} props: {}
}; };
} }
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift"; var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
console.log("GET PUBLISHER FROM:" + url) console.log("GET PUBLISHER FROM:" + url)
const { data: item } = await axios.get(url); const { data: item } = await axios.get(url);

View File

@ -136,6 +136,7 @@ export default function ImportPage() {
const handleSave = async () => { const handleSave = async () => {
try { try {
common.logger.debug("handleSave to: " + common.getBaseUrl()); common.logger.debug("handleSave to: " + common.getBaseUrl());
console.log("handleSave to: " + common.getBaseUrl());
const header = rawData[mode.headerRow]; const header = rawData[mode.headerRow];
for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher
//update status.info with current publisher //update status.info with current publisher
@ -194,7 +195,7 @@ export default function ImportPage() {
let personNames = names.join(' '); let personNames = names.join(' ');
try { try {
try { try {
const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isactive,type,availabilities"; const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isActive,type,availabilities";
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`); const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
let existingPublisher = responseByName.data[0]; let existingPublisher = responseByName.data[0];
if (!existingPublisher) { if (!existingPublisher) {
@ -248,7 +249,7 @@ export default function ImportPage() {
{ key: 'phone', value: phone }, { key: 'phone', value: phone },
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt }, { key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
{ key: 'isTrained', value: isTrained }, { key: 'isTrained', value: isTrained },
{ key: 'isactive', value: isActive }, { key: 'isActive', value: isActive },
{ key: 'type', value: publisherType, parse: common.getPubTypeEnum } { key: 'type', value: publisherType, parse: common.getPubTypeEnum }
]; ];
@ -286,7 +287,7 @@ export default function ImportPage() {
phone, phone,
firstName: names[0], firstName: names[0],
lastName: names[1], lastName: names[1],
isactive: isActive, isActive: isActive,
isTrained, isTrained,
desiredShiftsPerMonth, desiredShiftsPerMonth,
}); });
@ -301,7 +302,7 @@ export default function ImportPage() {
phone, phone,
firstName: firstname, firstName: firstname,
lastName: names[names.length - 1], lastName: names[names.length - 1],
isactive: isActive, isActive: isActive,
isTrained, isTrained,
desiredShiftsPerMonth desiredShiftsPerMonth
}); });
@ -530,7 +531,7 @@ export default function ImportPage() {
weekOfMonth: weekNr, // Add the missing 'weekOfMonth' weekOfMonth: weekNr, // Add the missing 'weekOfMonth'
startTime, startTime,
endTime, endTime,
isactive: true, isActive: true,
type: AvailabilityType.OneTime, type: AvailabilityType.OneTime,
isWithTransportIn: false, // Add the missing 'isWithTransport' property isWithTransportIn: false, // Add the missing 'isWithTransport' property
isWithTransportOut: false, // Add the missing 'isWithTransport' property isWithTransportOut: false, // Add the missing 'isWithTransport' property

View File

@ -225,7 +225,7 @@ export default PublishersPage;
export const getServerSideProps = async (context) => { export const getServerSideProps = async (context) => {
const axios = await axiosServer(context); const axios = await axiosServer(context);
//ToDo: refactor all axios calls to use axiosInstance and this URL //ToDo: refactor all axios calls to use axiosInstance and this URL
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isactive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect'); const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isActive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
return { return {
props: { props: {

View File

@ -47,7 +47,7 @@ export const getServerSideProps = async (context) => {
props: {} props: {}
}; };
} }
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts"; var url = process.env.NEXT_PUBLIC_PUBLIC_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts";
console.log("GET PUBLISHER FROM:" + url) console.log("GET PUBLISHER FROM:" + url)
const { data } = await axios.get(url); const { data } = await axios.get(url);

View File

@ -41,7 +41,7 @@ function ContactsPage({ publishers }) {
<tbody> <tbody>
{filteredPublishers.map((pub) => ( {filteredPublishers.map((pub) => (
<tr key={pub.id}> <tr key={pub.id}>
<td className="border-b p-4 pl-8">{pub.firstName} {pub.lastName}</td> <td className="border-b p-4 pl-8" title={pub.lastUpdate}>{pub.firstName} {pub.lastName}</td>
<td className="border-b p-4"> <td className="border-b p-4">
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} > <span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0} {pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
@ -74,16 +74,23 @@ export default ContactsPage;
export const getServerSideProps = async (context) => { export const getServerSideProps = async (context) => {
const dateStr = new Date().toISOString().split('T')[0]; const dateStr = new Date().toISOString().split('T')[0];
let publishers = await filterPublishers('id,firstName,lastName,email,isactive,desiredShiftsPerMonth', "", new Date(), true, true, false); let publishers = await filterPublishers('id,firstName,lastName,email,isActive,desiredShiftsPerMonth', "", new Date(), true, true, false);
// const axios = await axiosServer(context); // const axios = await axiosServer(context);
// const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`); // const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
// api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2Cisactive%2CdesiredShiftsPerMonth // api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2CisActive%2CdesiredShiftsPerMonth
publishers.forEach(publisher => { publishers.forEach(publisher => {
publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0; publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0;
publisher.assignments = publisher.assignments || []; publisher.assignments = publisher.assignments || [];
publisher.availabilities = publisher.availabilities || []; publisher.availabilities = publisher.availabilities || [];
publisher.lastUpdate = publisher.availabilities.reduce((acc, curr) => curr.dateOfEntry > acc ? curr.dateOfEntry : acc, null);
if (publisher.lastUpdate) {
publisher.lastUpdate = common.getDateFormated(publisher.lastUpdate);
}
else {
publisher.lastUpdate = "Няма данни";
}
//serialize dates in publisher.assignments and publisher.availabilities //serialize dates in publisher.assignments and publisher.availabilities
publisher.assignments.forEach(assignment => { publisher.assignments.forEach(assignment => {
if (assignment.shift && assignment.shift.startTime) { if (assignment.shift && assignment.shift.startTime) {
@ -100,8 +107,13 @@ export const getServerSideProps = async (context) => {
} }
} }
}); });
//remove availabilities that isFromPreviousAssignment
publisher.availabilities = publisher.availabilities.filter(availability => !availability.isFromPreviousAssignment);
}); });
//remove publishers without availabilities
publishers = publishers.filter(publisher => publisher.availabilities.length > 0);
return { return {
props: { props: {

View File

@ -32,7 +32,7 @@ export const getServerSideProps = async (context) => {
// }; // };
// } // }
// const { data: loc } = await axiosInstance.get( // const { data: loc } = await axiosInstance.get(
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id // `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id
// ); // );
// console.log(location) //this is the location object // console.log(location) //this is the location object

View File

@ -74,6 +74,18 @@ export default function Reports() {
Добави нов отчет Добави нов отчет
</button> </button>
</Link> </Link>
<label className="mr-4">
<input type="radio" name="reportType" value="ServiceReport" defaultChecked />
Отчети
</label>
<label className="mr-4">
<input type="radio" name="reportType" value="Experience" />
Случка
</label>
<label className="mr-4">
<input type="radio" name="reportType" value="Feedback" />
Отзиви
</label>
<div className="mt-4 w-full overflow-x-auto"> <div className="mt-4 w-full overflow-x-auto">
<table className="w-full table-auto"> <table className="w-full table-auto">
<thead> <thead>

View File

@ -33,7 +33,7 @@ export const getServerSideProps = async (context) => {
// } // }
// const { data: loc } = await axiosInstance.get( // const { data: loc } = await axiosInstance.get(
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id // `${process.env.NEXT_PUBLIC_PUBLIC_URL}api/data/locations/` + context.params.id
// ); // );
// console.log(location) //this is the location object // console.log(location) //this is the location object
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); // context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");

View File

@ -24,7 +24,7 @@ const ContactsPage = () => {
</div> </div>
</div > */ </div > */
} }
{/* <a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank"> {/* <a href="https://t.me/mwHitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
<img src="styles/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" /> <img src="styles/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
<span className="align-middle">Телеграм</span> <span className="align-middle">Телеграм</span>
</a> */} </a> */}

View File

@ -86,7 +86,7 @@ async function getAvailabilities(userId) {
select: { select: {
id: true, id: true,
name: true, name: true,
isactive: true, isActive: true,
isFromPreviousAssignment: true, isFromPreviousAssignment: true,
dayofweek: true, dayofweek: true,
dayOfMonth: true, dayOfMonth: true,
@ -121,7 +121,7 @@ async function getAvailabilities(userId) {
shiftId Int shiftId Int
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
publisherId String publisherId String
isactive Boolean @default(true) isActive Boolean @default(true)
isConfirmed Boolean @default(false) isConfirmed Boolean @default(false)
isWithTransport Boolean @default(false) isWithTransport Boolean @default(false)
Report Report[] Report Report[]
@ -133,7 +133,7 @@ async function getAvailabilities(userId) {
}, },
select: { select: {
id: true, id: true,
isTentative: true, isBySystem: true,
isConfirmed: true, isConfirmed: true,
isWithTransport: true, isWithTransport: true,
shift: { shift: {

View File

@ -13,7 +13,7 @@ export const publisherSelectWithAvCount = {
select: { select: {
availability: { availability: {
where: { where: {
isactive: true isActive: true
} }
} }
@ -40,7 +40,7 @@ export const publisherSelect = {
// endTime: true, // endTime: true,
// dayOfMonth: true, // dayOfMonth: true,
// dayofweek: true, // dayofweek: true,
// isactive: true, // isActive: true,
// count: 'Availability_count' // count: 'Availability_count'
// } // }
// } // }

View File

@ -5,7 +5,7 @@ CREATE TABLE `Publisher` (
`lastName` VARCHAR(191) NOT NULL, `lastName` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL, `email` VARCHAR(191) NOT NULL,
`phone` VARCHAR(191) NULL, `phone` VARCHAR(191) NULL,
`isactive` BOOLEAN NOT NULL DEFAULT true, `isActive` BOOLEAN NOT NULL DEFAULT true,
`age` INTEGER NULL, `age` INTEGER NULL,
UNIQUE INDEX `Publisher_email_key`(`email`), UNIQUE INDEX `Publisher_email_key`(`email`),
@ -41,7 +41,7 @@ CREATE TABLE `Shift` (
`name` VARCHAR(191) NOT NULL, `name` VARCHAR(191) NOT NULL,
`startTime` DATETIME(3) NOT NULL, `startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL, `endTime` DATETIME(3) NOT NULL,
`isactive` BOOLEAN NOT NULL DEFAULT true, `isActive` BOOLEAN NOT NULL DEFAULT true,
`requiresTransport` BOOLEAN NOT NULL DEFAULT false, `requiresTransport` BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
@ -52,7 +52,7 @@ CREATE TABLE `Location` (
`id` INTEGER NOT NULL AUTO_INCREMENT, `id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL, `name` VARCHAR(191) NOT NULL,
`address` VARCHAR(191) NOT NULL, `address` VARCHAR(191) NOT NULL,
`isactive` BOOLEAN NOT NULL DEFAULT true, `isActive` BOOLEAN NOT NULL DEFAULT true,
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL, `dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)

View File

@ -6,10 +6,10 @@
*/ */
-- AlterTable -- AlterTable
ALTER TABLE `Availability` ADD COLUMN `isactive` BOOLEAN NOT NULL DEFAULT true; ALTER TABLE `Availability` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true;
-- AlterTable -- AlterTable
ALTER TABLE `CartEvent` ADD COLUMN `isactive` BOOLEAN NOT NULL DEFAULT true, ALTER TABLE `CartEvent` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN `locationId` INTEGER NOT NULL, ADD COLUMN `locationId` INTEGER NOT NULL,
ADD COLUMN `shiftDuration` INTEGER NOT NULL; ADD COLUMN `shiftDuration` INTEGER NOT NULL;

View File

@ -18,7 +18,7 @@ CREATE TABLE `Assignment` (
`id` INTEGER NOT NULL AUTO_INCREMENT, `id` INTEGER NOT NULL AUTO_INCREMENT,
`shiftId` INTEGER NOT NULL, `shiftId` INTEGER NOT NULL,
`publisherId` INTEGER NOT NULL, `publisherId` INTEGER NOT NULL,
`isactive` BOOLEAN NOT NULL DEFAULT true, `isActive` BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -1,11 +1,11 @@
/* /*
Warnings: Warnings:
- You are about to drop the column `isactive` on the `assignment` table. All the data in the column will be lost. - You are about to drop the column `isActive` on the `assignment` table. All the data in the column will be lost.
*/ */
-- AlterTable -- AlterTable
ALTER TABLE `Assignment` DROP COLUMN `isactive`, ALTER TABLE `Assignment` DROP COLUMN `isActive`,
ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false; ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false;
-- AlterTable -- AlterTable

View File

@ -0,0 +1,21 @@
/*
Warnings:
- You are about to drop the column `isTentative` on the `Assignment` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Assignment`
ADD COLUMN `isBySystem` BOOLEAN NOT NULL DEFAULT FALSE;
-- Depending on your DBMS, you might need to execute one statement at a time.
-- Especially, the UPDATE statement should be run separately.
UPDATE `Assignment` SET `isBySystem` = isTentative;
-- Drop the isTentative column
ALTER TABLE `Assignment` DROP COLUMN `isTentative`;
-- AlterTable
ALTER TABLE `Report`
ADD COLUMN `type` ENUM('ServiceReport', 'Experience', 'Feedback_Problem', 'Feedback_Suggestion', 'Feedback') NOT NULL DEFAULT 'ServiceReport';

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `parentAvailabilityId` INTEGER NULL;
-- AddForeignKey
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_parentAvailabilityId_fkey` FOREIGN KEY (`parentAvailabilityId`) REFERENCES `Availability`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `isSubscribedToCoverMe` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `isSubscribedToReminders` BOOLEAN NOT NULL DEFAULT false;

View File

@ -81,13 +81,21 @@ enum PublisherType {
SpecialPioneer_Missionary SpecialPioneer_Missionary
} }
enum ReportType {
ServiceReport
Experience
Feedback_Problem
Feedback_Suggestion
Feedback
}
model Publisher { model Publisher {
id String @id @default(cuid()) id String @id @default(cuid())
firstName String firstName String
lastName String lastName String
email String @unique email String @unique
phone String? phone String?
isactive Boolean @default(true) isActive Boolean @default(true)
isImported Boolean @default(false) isImported Boolean @default(false)
isTrained Boolean @default(false) isTrained Boolean @default(false)
age Int? age Int?
@ -98,20 +106,21 @@ model Publisher {
userId String? @unique userId String? @unique
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
role UserRole @default(USER) role UserRole @default(USER)
desiredShiftsPerMonth Int @default(4) desiredShiftsPerMonth Int @default(4)
isMale Boolean @default(true) isMale Boolean @default(true)
isNameForeign Boolean @default(false) isNameForeign Boolean @default(false)
isSubscribedToCoverMe Boolean @default(false)
familyHeadId String? // Optional familyHeadId for each family member isSubscribedToReminders Boolean @default(false)
familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id]) familyHeadId String? // Optional familyHeadId for each family member
familyMembers Publisher[] @relation("FamilyMember") familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
alwaysAsFamily Boolean? @default(false) //NEW v1.0.1 // New field to indicate if the publisher always wants to be assigned with the family familyMembers Publisher[] @relation("FamilyMember")
type PublisherType @default(Publisher) alwaysAsFamily Boolean? @default(false) //NEW v1.0.1 // New field to indicate if the publisher always wants to be assigned with the family
town String? type PublisherType @default(Publisher)
comments String? town String?
reports Report[] comments String?
Message Message[] reports Report[]
Message Message[]
} }
model Availability { model Availability {
@ -124,7 +133,7 @@ model Availability {
weekOfMonth Int? weekOfMonth Int?
startTime DateTime startTime DateTime
endTime DateTime endTime DateTime
isactive Boolean @default(true) isActive Boolean @default(true)
type AvailabilityType @default(Weekly) type AvailabilityType @default(Weekly)
isWithTransportIn Boolean @default(false) isWithTransportIn Boolean @default(false)
isWithTransportOut Boolean @default(false) isWithTransportOut Boolean @default(false)
@ -133,7 +142,10 @@ model Availability {
repeatWeekly Boolean? // New field to indicate weekly repetition // until now dayofweek was used for repetition when dayOfMonth is null repeatWeekly Boolean? // New field to indicate weekly repetition // until now dayofweek was used for repetition when dayOfMonth is null
repeatFrequency Int? // New field to indicate repetition frequency repeatFrequency Int? // New field to indicate repetition frequency
endDate DateTime? // New field for the end date of repetition endDate DateTime? // New field for the end date of repetition
dateOfEntry DateTime? //NEW v1.0.1 trade storage for intuintivity dateOfEntry DateTime? //NEW v1.0.1
parentAvailabilityId Int?
parentAvailability Availability? @relation("ParentAvailability", fields: [parentAvailabilityId], references: [id])
ChildAvailabilities Availability[] @relation("ParentAvailability")
} }
model CartEvent { model CartEvent {
@ -143,7 +155,7 @@ model CartEvent {
shiftDuration Int shiftDuration Int
shifts Shift[] shifts Shift[]
dayofweek DayOfWeek dayofweek DayOfWeek
isactive Boolean @default(true) isActive Boolean @default(true)
location Location @relation(fields: [locationId], references: [id]) location Location @relation(fields: [locationId], references: [id])
locationId Int locationId Int
eventType EventType @default(PW_Cart) eventType EventType @default(PW_Cart)
@ -160,7 +172,7 @@ model Shift {
name String name String
startTime DateTime startTime DateTime
endTime DateTime endTime DateTime
isactive Boolean @default(true) isActive Boolean @default(true)
requiresTransport Boolean @default(false) requiresTransport Boolean @default(false)
notes String? notes String?
//date DateTime //date DateTime
@ -177,7 +189,7 @@ model Assignment {
shiftId Int shiftId Int
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
publisherId String publisherId String
isTentative Boolean @default(false) // if no availability for it, when importing previous schedules isBySystem Boolean @default(false) // if no availability for it, when importing previous schedules
isConfirmed Boolean @default(false) isConfirmed Boolean @default(false)
isWithTransport Boolean @default(false) isWithTransport Boolean @default(false)
isMailSent Boolean @default(false) isMailSent Boolean @default(false)
@ -190,7 +202,7 @@ model Location {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
address String address String
isactive Boolean @default(true) isActive Boolean @default(true)
content String? @db.LongText content String? @db.LongText
cartEvents CartEvent[] cartEvents CartEvent[]
reports Report[] reports Report[]
@ -219,7 +231,8 @@ model Report {
returnVisitInfoCount Int? returnVisitInfoCount Int?
conversationCount Int? conversationCount Int?
experienceInfo String? @db.LongText experienceInfo String? @db.LongText
type ReportType @default(ServiceReport)
@@map("Report") @@map("Report")
} }

View File

@ -30,7 +30,7 @@ INSERT INTO
`id`, `id`,
`name`, `name`,
`address`, `address`,
`isactive` `isActive`
) )
VALUES ( VALUES (
1, 1,
@ -57,7 +57,7 @@ INSERT INTO
`startTime`, `startTime`,
`endTime`, `endTime`,
`dayofweek`, `dayofweek`,
`isactive`, `isActive`,
`locationId`, `locationId`,
`shiftDuration`, `shiftDuration`,
`eventType`, `eventType`,
@ -147,7 +147,7 @@ VALUES (
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */ /*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */
; ;
-- INSERT INTO `cartevent` (`id`, `startTime`, `endTime`, `dayofweek`, `isactive`, `locationId`, `shiftDuration`, `eventType`, `numberOfPublishers`) -- INSERT INTO `cartevent` (`id`, `startTime`, `endTime`, `dayofweek`, `isActive`, `locationId`, `shiftDuration`, `eventType`, `numberOfPublishers`)
-- VALUES -- VALUES
-- (2, '2023-12-27 07:00:33.174', '2023-12-27 16:00:33.174', 'Tuesday', 1, 2, 90, 'PW_Cart', 4), -- (2, '2023-12-27 07:00:33.174', '2023-12-27 16:00:33.174', 'Tuesday', 1, 2, 90, 'PW_Cart', 4),
-- (3, '2023-12-28 07:00:33.174', '2023-12-28 16:00:33.174', 'Wednesday', 1, 3, 90, 'PW_Cart', 4), -- (3, '2023-12-28 07:00:33.174', '2023-12-28 16:00:33.174', 'Wednesday', 1, 3, 90, 'PW_Cart', 4),

2
process.d.ts vendored
View File

@ -1,6 +1,6 @@
declare namespace NodeJS { declare namespace NodeJS {
export interface ProcessEnv { export interface ProcessEnv {
NEXTAUTH_URL: string PUBLIC_URL: string
NEXTAUTH_SECRET: string NEXTAUTH_SECRET: string
GITHUB_ID: string GITHUB_ID: string
GITHUB_SECRET: string GITHUB_SECRET: string

View File

@ -28,22 +28,25 @@ let baseUrlGlobal;
// require('dotenv').config(); // require('dotenv').config();
// } // }
console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param
require('dotenv').config({ require('dotenv').config({
path: `.env.${process.env.NODE_ENV}` path: `.env.${process.env.NODE_ENV}`
}); });
console.log("process.env.NODE_ENV = ", process.env.NODE_ENV); console.log("process.env.NODE_ENV = ", process.env.NODE_ENV);
const PORT = process.env.NEXT_PUBLIC_PORT || 3000; const PROTOCOL = process.env.PROTOCOL;
const HOST = process.env.NEXT_PUBLIC_HOST; const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST;
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "production";
const PROTOCOL = process.env.NEXT_PUBLIC_PROTOCOL;
const nextApp = next({ dev }); const nextApp = next({ dev });
const nextHandler = nextApp.getRequestHandler(); const nextHandler = nextApp.getRequestHandler();
console.log("process.env.SSL_ENABLED = ", process.env.SSL_ENABLED); console.log("process.env.PROTOCOL = ", process.env.PROTOCOL);
process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_PUBLIC_URL; //NEXTAUTH_URL mandatory for next-auth
console.log("process.env.NEXT_PUBLIC_PUBLIC_URL = ", process.env.NEXT_PUBLIC_PUBLIC_URL);
console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL); console.log("process.env.NEXTAUTH_URL = ", process.env.NEXTAUTH_URL);
console.log("process.env.NEXT_PUBLIC_PORT = ", process.env.NEXT_PUBLIC_PORT); console.log("process.env.PORT = ", process.env.PORT);
console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT); console.log("process.env.TELEGRAM_BOT = ", process.env.TELEGRAM_BOT);
//require('module-alias/register'); //require('module-alias/register');
@ -62,12 +65,37 @@ const uploadTmp = multer({ storage: storageMem });
const prisma = common.getPrismaClient(); const prisma = common.getPrismaClient();
const server = express();
//check if ssl is enabled
if (process.env.PROTOCOL === 'https') {
console.log("SSL_ENABLED = true");
// Redirect from http to https
// server.use((req, res, next) => {
// if (req.headers['x-forwarded-proto'] !== 'https') {
// return res.redirect(`https://${req.headers.host}${req.url}`);
// }
// next();
// });
if (process.env.SSL_KEY && process.env.SSL_CERT) {
const options = {
key: fs.readFileSync(process.env.SSL_KEY),
cert: fs.readFileSync(process.env.SSL_CERT),
secureProtocol: 'TLSv1_2_method', // Example: Force TLS 1.2
};
https.createServer(options, server).listen(PORT);
}
}
else {
server.listen(PORT, (err) => {
if (err) throw err;
console.log(`> Ready on ${PROTOCOL}://${HOST}:${PORT}`);
});
}
// handlers // handlers
nextApp nextApp
.prepare() .prepare()
.then(() => { .then(() => {
const server = express();
// Add the middleware to set 'x-forwarded-host' header // Add the middleware to set 'x-forwarded-host' header
server.use((req, res, next) => { server.use((req, res, next) => {
@ -84,6 +112,8 @@ nextApp
next(); next();
}); });
server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico")); server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico"));
// server.use("/robots.txt", express.static("styles/favicon_io/robots.txt"));
// server.use("/sitemap.xml", express.static("styles/favicon_io/sitemap.xml"));
server.get("/last_schedule_json", (req, res) => { server.get("/last_schedule_json", (req, res) => {
// var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8")); // var data = JSON.parse(fs.readFileSync("./content/sources/march_flat.json", "utf8"));
@ -259,7 +289,7 @@ nextApp
var shifts = await prisma.shift.findMany({ var shifts = await prisma.shift.findMany({
where: { where: {
isactive: true, isActive: true,
startTime: { startTime: {
gte: fromDate, gte: fromDate,
lt: toDate, lt: toDate,
@ -410,7 +440,7 @@ nextApp
var publishers = await prisma.publisher.findMany({ var publishers = await prisma.publisher.findMany({
where: { where: {
isactive: true, isActive: true,
email: { email: {
not: "", not: "",
}, },
@ -556,31 +586,6 @@ nextApp
return nextHandler(req, res); return nextHandler(req, res);
}); });
//check if ssl is enabled
if (process.env.SSL_ENABLED === "true") {
console.log("SSL_ENABLED = true");
// Redirect from http to https
// server.use((req, res, next) => {
// if (req.headers['x-forwarded-proto'] !== 'https') {
// return res.redirect(`https://${req.headers.host}${req.url}`);
// }
// next();
// });
if (process.env.SSL_KEY && process.env.SSL_CERT) {
const options = {
key: fs.readFileSync(process.env.SSL_KEY),
cert: fs.readFileSync(process.env.SSL_CERT),
secureProtocol: 'TLSv1_2_method', // Example: Force TLS 1.2
};
https.createServer(options, server).listen(PORT);
}
}
else {
server.listen(PORT, (err) => {
if (err) throw err;
console.log(`> Ready on ${PROTOCOL}://${HOST}:${PORT}`);
});
}
}) })
.catch((ex) => { .catch((ex) => {
console.warn(`Error starting server on ${HOST}:${PORT}`) console.warn(`Error starting server on ${HOST}:${PORT}`)

View File

@ -4,8 +4,9 @@
const levenshtein = require('fastest-levenshtein'); const levenshtein = require('fastest-levenshtein');
const fs = require("fs"); const fs = typeof window === 'undefined' ? require('fs') : undefined;
const path = require("path"); const path = typeof window === 'undefined' ? require('path') : undefined;
const { PrismaClient } = require('@prisma/client'); const { PrismaClient } = require('@prisma/client');
const DayOfWeek = require("@prisma/client").DayOfWeek; const DayOfWeek = require("@prisma/client").DayOfWeek;
@ -75,43 +76,10 @@ exports.setBaseUrl = function (req) {
}; };
exports.getBaseUrl = function (relative = "", req = null) { exports.getBaseUrl = function (relative = "", req = null) {
const filePath = path.join(__dirname, 'baseUrl.txt'); return process.env.NEXT_PUBLIC_PUBLIC_URL + relative;
try {
if (fs.existsSync(filePath)) {
const baseUrl = fs.readFileSync(filePath, 'utf8').trim();
const fullUrl = relative ? new URL(relative, baseUrl).toString() : baseUrl;
return fullUrl;
} else {
if (req) {
const baseUrl = exports.setBaseUrl(req); // Correctly reference setBaseUrl
return `${baseUrl}/${relative.replace(/^\/|\/$/g, '')}`;
}
console.log('Base URL file does not exist.');
return null;
}
} catch (error) {
console.error('Error reading the base URL file:', error);
return null;
}
// const host = process.env.NEXT_PUBLIC_HOST || '127.0.0.1';
// const port = process.env.NEXT_PUBLIC_PORT ? `:${ process.env.NEXT_PUBLIC_PORT } ` : '';
// const protocol = process.env.NEXT_PUBLIC_PROTOCOL || "https"
// //const url = `${ protocol }://${host}${port}/${relative.replace(/^\/|\/$/g, '')}/`;
// const isRelativeEmpty = !relative || relative.trim() === '';
// const formattedRelative = !isRelativeEmpty ? '/' + relative.replace(/^\/|\/$/g, '') : '';
// const url = `${protocol}://${host}${port}${formattedRelative}`;
// logger.debug("NODE_ENV = ", process.env.NODE_ENV, "protocol:", protocol);
// logger.debug("getBaseURL = ", url);
// return url;
}; };
let prisma; let prisma;
exports.getPrismaClient = function getPrismaClient() { exports.getPrismaClient = function getPrismaClient() {
if (!prisma) { if (!prisma) {
@ -122,6 +90,8 @@ exports.getPrismaClient = function getPrismaClient() {
datasources: { db: { url: process.env.DATABASE_URL } }, datasources: { db: { url: process.env.DATABASE_URL } },
}); });
} }
logger.debug("getPrismaClient: process.env.DATABASE_URL = ", process.env.DATABASE_URL);
return prisma; return prisma;
} }
@ -228,15 +198,52 @@ exports.getDayOfWeekDate = function (dayOfWeekName, date = new Date()) {
return date; return date;
}; };
//common.getWeekOfMonth(date) //common.getWeekOfMonth(date)
// exports.getWeekOfMonth = function (date) { exports.getWeekOfMonth = function (inputDate) {
// // Copy date so don't modify original let date = new Date(inputDate);
// date = new Date(date); let firstDayOfMonth = new Date(date.getFullYear(), date.getMonth(), 1);
// // Adjust to Monday of this week let firstMonday = new Date(firstDayOfMonth);
// date.setDate(date.getDate() + 3 - (date.getDayEuropean() + 6) % 7);
// // Return week number // Adjust firstDayOfMonth to the first Monday of the month
// const weekNumber = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 / 7); if (firstDayOfMonth.getDay() === 0) { // Sunday
// return weekNumber; firstMonday.setDate(2);
// } } else if (firstDayOfMonth.getDay() !== 1) { // Not Monday
firstMonday.setDate(9 - firstDayOfMonth.getDay());
}
// Calculate the difference in days
let diff = (date - firstMonday) / (1000 * 60 * 60 * 24);
// Calculate week number
let weekNumber = Math.ceil((diff + 1) / 7);
return weekNumber;
};
exports.getDateFromWeekNrAndDayOfWeek = function (firstMonday, weekNr, dayOfWeekEnum, startTime) {
firstMonday = new Date(firstMonday);
startTime = new Date(startTime);
if (!weekNr || weekNr < 1 || weekNr > 5) {
weekNr = this.getWeekOfMonth(startTime);
}
//get int from dayOfWeekEnum
let dayOfWeekNr = this.getDayOfWeekIndex(dayOfWeekEnum);
if (dayOfWeekNr < 0 || dayOfWeekNr > 6) {
dayOfWeekNr = 0;
}
// Calculate the day offset from the first Monday of the month
// Note: Assuming dayOfWeekEnum starts from 0 (Monday) to 6 (Sunday)
const daysFromFirstMonday = (weekNr - 1) * 7 + dayOfWeekNr;
// Calculate the new date
let newStart = new Date(firstMonday);
newStart.setDate(firstMonday.getDate() + daysFromFirstMonday);
// Extract time from startTime and apply it to newStart
const time = new Date(startTime);
newStart.setHours(time.getHours(), time.getMinutes(), time.getSeconds());
return newStart;
}
exports.getMonthDatesInfo = function (date) { exports.getMonthDatesInfo = function (date) {
// get first day of the month // get first day of the month
@ -275,6 +282,9 @@ exports.getMonthDatesInfo = function (date) {
// lastSunday.setDate(firstDayNextMonth.getDate() - firstDayNextMonth.getDay()); // lastSunday.setDate(firstDayNextMonth.getDate() - firstDayNextMonth.getDay());
//logger.debug("Last Sunday: ", lastSunday); //logger.debug("Last Sunday: ", lastSunday);
const diffInDays = (lastSunday - firstMonday) / (1000 * 60 * 60 * 24);
// Calculate number of weeks, rounding up for partial weeks
const nrOfWeeks = Math.ceil((diffInDays + 1) / 7);
return { return {
firstDay: firstDay, firstDay: firstDay,
@ -285,7 +295,7 @@ exports.getMonthDatesInfo = function (date) {
date: date, date: date,
monthName: monthName, monthName: monthName,
year: date.getFullYear(), year: date.getFullYear(),
nrOfWeeks: Math.ceil((lastMonday.getDate() - firstMonday.getDate()) / 7) nrOfWeeks: nrOfWeeks
}; };
}; };
exports.getMonthInfo = exports.getMonthDatesInfo; exports.getMonthInfo = exports.getMonthDatesInfo;

View File

@ -79,10 +79,7 @@ async function findPublisher(names, email, select, getAll = false) {
} }
async function findPublisherAvailability(publisherId, date) { async function findPublisherAvailability(publisherId, date) {
const prisma = common.getPrismaClient(); const prisma = common.getPrismaClient();
const dayOfWeek = common.getDayOfWeekNameEnEnum(date); // Assuming common.getDayOfWeekNameEnEnum returns the day of week
//const weekOfMonth = common.getWeekOfMonth(date); // Assuming common.getWeekOfMonth returns the week of month
date = new Date(date); // Convert to date object if not already date = new Date(date); // Convert to date object if not already
const hours = date.getHours(); const hours = date.getHours();
const minutes = date.getMinutes(); const minutes = date.getMinutes();
@ -90,32 +87,24 @@ async function findPublisherAvailability(publisherId, date) {
const potentialAvailabilities = await prisma.availability.findMany({ const potentialAvailabilities = await prisma.availability.findMany({
where: { where: {
publisherId: publisherId, publisherId: publisherId,
OR: [ AND: [ // Ensure both conditions must be met
{ {
// Exact date match
startTime: { startTime: {
gte: new Date(date.setHours(0, 0, 0, 0)), lte: new Date(date), // startTime is less than or equal to the date
lt: new Date(date.setHours(23, 59, 59, 999)) },
}
}, },
{ {
// Correct day of week and before the date, with endDate consideration endTime: {
dayofweek: dayOfWeek, gte: new Date(date), // endTime is greater than or equal to the date
OR: [ },
{ },
endDate: null ],
},
{
endDate: {
gt: date
}
}
]
}
]
} }
}); });
if (potentialAvailabilities.length === 0) {
return null; // No availability found
}
// Filter the results based on time and other criteria when not exact date match // Filter the results based on time and other criteria when not exact date match
const availability = potentialAvailabilities.find(avail => { const availability = potentialAvailabilities.find(avail => {
const availStartHours = avail.startTime.getHours(); const availStartHours = avail.startTime.getHours();
@ -147,7 +136,7 @@ async function getAvailabilities(userId) {
select: { select: {
id: true, id: true,
name: true, name: true,
isactive: true, isActive: true,
isFromPreviousAssignment: true, isFromPreviousAssignment: true,
dayofweek: true, dayofweek: true,
dayOfMonth: true, dayOfMonth: true,
@ -182,7 +171,7 @@ async function getAvailabilities(userId) {
shiftId Int shiftId Int
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
publisherId String publisherId String
isactive Boolean @default(true) isActive Boolean @default(true)
isConfirmed Boolean @default(false) isConfirmed Boolean @default(false)
isWithTransport Boolean @default(false) isWithTransport Boolean @default(false)
Report Report[] Report Report[]
@ -194,7 +183,7 @@ async function getAvailabilities(userId) {
}, },
select: { select: {
id: true, id: true,
isTentative: true, isBySystem: true,
isConfirmed: true, isConfirmed: true,
isWithTransport: true, isWithTransport: true,
shift: { shift: {

View File

@ -323,21 +323,21 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
} }
var shifts = await prisma.shift.findMany({ var shifts = await prisma.shift.findMany({
where: { where: {
isactive: true, isActive: true,
startTime: { startTime: {
gte: monthDatesInfo.firstMonday, gte: monthDatesInfo.firstMonday,
lt: monthDatesInfo.lastSunday, lt: monthDatesInfo.lastSunday,
}, },
} }
}); });
var locations = await prisma.location.findMany({ where: { isactive: true, } }); var locations = await prisma.location.findMany({ where: { isActive: true, } });
var cartEvents = await prisma.cartEvent.findMany({ where: { isactive: true, } }); var cartEvents = await prisma.cartEvent.findMany({ where: { isActive: true, } });
var publishers = await prisma.publisher.findMany({ var publishers = await prisma.publisher.findMany({
where: { isactive: true, }, where: { isActive: true, },
include: { include: {
availabilities: { where: { isactive: true, }, }, availabilities: { where: { isActive: true, }, },
assignments: { include: { shift: true, }, }, assignments: { include: { shift: true, }, },
}, },
}); });
@ -472,7 +472,7 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
email: name.toLowerCase().replace(/ /g, "."), // + "@gmail.com" email: name.toLowerCase().replace(/ /g, "."), // + "@gmail.com"
firstName: firstname, firstName: firstname,
lastName: lastname, lastName: lastname,
isactive: true, isActive: true,
isImported: true, isImported: true,
// role: "EXTERNAL", // role: "EXTERNAL",
}; };
@ -486,18 +486,18 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
// const availability = await prisma.availability.create({ // const availability = await prisma.availability.create({
// data: { // data: {
// publisherId: publisher.id, // publisherId: publisher.id,
// //date: date,
// dayofweek: dayofWeek, // dayofweek: dayofWeek,
// startTime: startTime, // startTime: startTime,
// endTime: endTime, // endTime: endTime,
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`, // name: `от график, ${publisher.firstName} ${publisher.lastName}`,
// isFromPreviousAssignment: true, // isFromPreviousAssignment: true,
// isactive: true, // isActive: true,
// }, // },
// }); // });
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`); // console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
// } // }
// const personResponse = await axiosInstance.post("/publishers", manualPub);
const personResponse = await axiosInstance.post("/publishers", manualPub);
// let personId = personResponse.data.id; // let personId = personResponse.data.id;
} catch (e) { } catch (e) {
@ -533,6 +533,7 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
}); });
//ToDo: fix findPublisherAvailability and creation of availabilities //ToDo: fix findPublisherAvailability and creation of availabilities
// check if there is an availability for this publisher on this date, and if not, create one // check if there is an availability for this publisher on this date, and if not, create one
//ToDo: check if that works
// const availability = await data.findPublisherAvailability(publisher.id, start); // const availability = await data.findPublisherAvailability(publisher.id, start);
// if (!availability && createAvailabilities) { // if (!availability && createAvailabilities) {
// const dayofWeek = common.getDayOfWeekNameEnEnum(date); // const dayofWeek = common.getDayOfWeekNameEnEnum(date);
@ -546,9 +547,11 @@ exports.processEvents = async function (events, year, monthNumber, progressCallb
// endTime: end, // endTime: end,
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`, // name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`,
// isFromPreviousAssignment: true, // isFromPreviousAssignment: true,
// isWithTransportIn: isWithTransport && event.shiftNr == 1,
// isWithTransportOut: isWithTransport && event.shiftNr > 1,
// }, // },
// }); // });
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`); // console.log(`Created SYSTEM availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
// } // }
console.log(`Created assignment with ID ${assignment.id} for date '${date.toDateString()}' and location '${event.placeOfEvent}'. publisher: ${publisher.firstName} ${publisher.lastName}}`); console.log(`Created assignment with ID ${assignment.id} for date '${date.toDateString()}' and location '${event.placeOfEvent}'. publisher: ${publisher.firstName} ${publisher.lastName}}`);

View File

@ -1,4 +0,0 @@
//??? can we consolidate all imports into one file?
import ProtectedRoute from '../../../components/protectedRoute';
import axiosInstance from '../../../src/axiosSecure';
import Layout from "../../../components/layout";

View File

@ -11,7 +11,7 @@ SELECT DISTINCT Publisher.*
FROM Publisher FROM Publisher
INNER JOIN Availability ON Availability.publisherId = Publisher.id INNER JOIN Availability ON Availability.publisherId = Publisher.id
WHERE WHERE
Availability.isactive = true Availability.isActive = true
AND ( ( AND ( (
Availability.dayOfMonth IS NOT NULL Availability.dayOfMonth IS NOT NULL
AND Availability.startTime <= '2023-03-30 13:00:00' AND Availability.startTime <= '2023-03-30 13:00:00'
@ -30,7 +30,7 @@ SELECT DISTINCT Publisher.*
FROM Publisher FROM Publisher
INNER JOIN Availability ON Availability.publisherId = Publisher.id INNER JOIN Availability ON Availability.publisherId = Publisher.id
WHERE WHERE
Availability.isactive = true Availability.isActive = true
AND (Availability.dayOfMonth = 5) clfuyo33e005aknvchf1wm3bu All publishers: 121; AND (Availability.dayOfMonth = 5) clfuyo33e005aknvchf1wm3bu All publishers: 121;
(118) unique, (118) unique,