Merge commit 'd3ade91b80af2e8b7b93c15428baa02109a3c3dc' into production

This commit is contained in:
Dobromir Popov
2024-05-28 18:22:13 +03:00
9 changed files with 390 additions and 207 deletions

View File

@ -9,7 +9,7 @@ const common = require('src/helpers/common');
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) { function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo, onAssignmentChange }) {
const [isDeleted, setIsDeleted] = useState(false); const [isDeleted, setIsDeleted] = useState(false);
const [assignments, setAssignments] = useState(shift.assignments); const [assignments, setAssignments] = useState(shift.assignments);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@ -59,7 +59,11 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
try { try {
console.log("Removing assignment with id:", id); console.log("Removing assignment with id:", id);
await axiosInstance.delete("/api/data/assignments/" + id); await axiosInstance.delete("/api/data/assignments/" + id);
let assingmnt = assignments.find(ass => ass.id == id);
setAssignments(prevAssignments => prevAssignments.filter(ass => ass.id !== id)); setAssignments(prevAssignments => prevAssignments.filter(ass => ass.id !== id));
if (onAssignmentChange) {
onAssignmentChange(assingmnt.publisherId, 'remove');
}
} catch (error) { } catch (error) {
console.error("Error removing assignment:", error); console.error("Error removing assignment:", error);
} }
@ -77,6 +81,12 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment); const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
// Update the 'publisher' property of the returned data with the full publisher object // Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher; data.publisher = publisher;
//ToDo: see if we need to update in state
// publisher.currentWeekAssignments += 1;
// publisher.currentMonthAssignments += 1;
if (onAssignmentChange) {
onAssignmentChange(data.publisher.id, 'add')
}
setAssignments(prevAssignments => [...prevAssignments, data]); setAssignments(prevAssignments => [...prevAssignments, data]);
} catch (error) { } catch (error) {
console.error("Error adding assignment:", error); console.error("Error adding assignment:", error);
@ -151,49 +161,45 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
let borderStyles = ''; let borderStyles = '';
let canTransport = false; let canTransport = false;
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) { if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers borderStyles += 'bg-blue-200 font-bold '; // Bottom border for selected publishers
} }
else {
if (publisherInfo.availabilityCount == 0) //user has never the form if (publisherInfo.availabilityCount == 0 || !publisherInfo.availabilities || publisherInfo.availabilities.length == 0)
{ //user has never the filled the avalabilities form or if there is no publisherInfo - draw red border - publisher is no longer available for the day!
borderStyles = 'border-2 border-orange-300 '; {
borderStyles += 'border-2 border-red-500 ';
} else {
// checkig if the publisher is available for this assignment
//ToDo: verify if that check is correct
const av = publisherInfo.availabilities?.find(av =>
av.startTime <= shift.startTime && av.endTime >= shift.endTime
);
if (av) {
borderStyles += 'border-b-2 border-blue-500 '; // Left border for specific availability conditions
ass.canTransport = av.isWithTransportIn || av.isWithTransportOut;
// console.log(publisherInfo.firstName, "available for shift", shift.id, "at", common.getDateFormattedShort(new Date(shift.startTime)), "av-" + av.id, ": a." + common.getTimeFormatted(av.startTime), "<=", common.getTimeFormatted(shift.startTime), "; a." + common.getTimeFormatted(av.endTime), ">=", common.getTimeFormatted(shift.endTime));
}
else {
borderStyles += 'border-l-4 border-red-500 ';
} }
else
//if there is no publisherInfo - draw red border - publisher is no longer available for the day!
if (!publisherInfo.availabilities || publisherInfo.availabilities.length == 0) {
borderStyles = 'border-2 border-red-500 ';
}
else {
// checkig if the publisher is available for this assignment if (publisherInfo.hasUpToDateAvailabilities) {
const av = publisherInfo.availabilities?.find(av => //add green right border
av.startTime <= shift.startTime && av.endTime >= shift.endTime borderStyles += 'border-r-2 border-green-300';
); }
if (av) {
borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions
ass.canTransport = av.isWithTransportIn || av.isWithTransportOut;
}
else {
borderStyles += 'border-l-4 border-red-500 ';
}
if (publisherInfo.hasUpToDateAvailabilities) { //the pub is the same time as last month
//add green right border // if (publisherInfo.availabilities?.some(av =>
borderStyles += 'border-r-2 border-green-300'; // (!av.dayOfMonth || av.isFromPreviousMonth) &&
} // av.startTime <= ass.startTime &&
// av.endTime >= ass.endTime)) {
//the pub is the same time as last month // borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions
// if (publisherInfo.availabilities?.some(av => // }
// (!av.dayOfMonth || av.isFromPreviousMonth) &&
// av.startTime <= ass.startTime &&
// av.endTime >= ass.endTime)) {
// borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions
// }
}
} }
return ( return (
<div key={index} <div key={index}
className={`flow rounded-md px-2 py-1 sm:py-0.5 my-1 ${(ass.isConfirmed && !ass.isBySystem) ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`} className={`flow rounded-md px-2 py-1 sm:py-0.5 my-1 ${(ass.isConfirmed && !ass.isBySystem) ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}

View File

@ -6,7 +6,7 @@ import { DayOfWeek, AvailabilityType, UserRole, EventLogType } from '@prisma/cli
const common = require('../../src/helpers/common'); const common = require('../../src/helpers/common');
const dataHelper = require('../../src/helpers/data'); const dataHelper = require('../../src/helpers/data');
const subq = require('../../prisma/bl/subqueries'); const subq = require('../../prisma/bl/subqueries');
import { addMinutes } from 'date-fns'; import { set, format, addMinutes, addDays } from 'date-fns';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
@ -147,9 +147,40 @@ export default async function handler(req, res) {
res.status(200).json(events); res.status(200).json(events);
case "getPublisherInfo": case "getPublisherInfo":
let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", null, req.query.assignments || true, req.query.availabilities || true, false, req.query.id);
res.status(200).json(pubs[0]); // let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", null, req.query.assignments || true, req.query.availabilities || true, false, req.query.id);
let pubs = await dataHelper.filterPublishersNew("id,firstName,lastName,email,isActive,assignments,availabilities", day, false, true, false, true, false, req.query.id, true);
let pub = pubs[0] || {};
if (pub) {
let dayOfWeekQuery = common.getDayOfWeek(day);
pub.availabilities = pub.availabilities.map(avail => {
if (avail.dayOfMonth == null) {
let dayOfWeek = common.getDayOfWeekIndex(avail.dayofweek);
let newStart = new Date(day);
newStart = addDays(newStart, dayOfWeek - dayOfWeekQuery);
newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
let newEnd = new Date(newStart);
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
return {
...avail,
startTime: newStart,
endTime: newEnd
}
}
return avail;
});
}
//let pubs = await dataHelper.filterPublishersNew("id,firstName,lastName,email,isActive,assignments,availabilities", day, false, true, false, true, false, req.query.id);
res.status(200).json(pub);
break; break;
case "filterPublishersNew":
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
let results = await filterPublishersNew_Available(req.query.select, day,
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities, req.query.id);
res.status(200).json(results);
break;
case "getMonthlyStatistics": case "getMonthlyStatistics":
let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day); let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day);
res.status(200).json(allpubs); res.status(200).json(allpubs);
@ -168,12 +199,6 @@ export default async function handler(req, res) {
//!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName))); //!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName)));
res.status(200).json(publishers); res.status(200).json(publishers);
break; break;
case "filterPublishersNew":
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
let results = await filterPublishersNew_Available(req.query.select, day,
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities);
res.status(200).json(results);
break;
// find publisher by full name or email // find publisher by full name or email
case "findPublisher": case "findPublisher":

View File

@ -3,6 +3,8 @@
import axiosServer from '../../src/axiosServer'; import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { set, format, addDays } from 'date-fns';
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client"; import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
import { levenshteinEditDistance } from "levenshtein-edit-distance"; import { levenshteinEditDistance } from "levenshtein-edit-distance";
@ -54,7 +56,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
req.query.date?.toString() || common.getISODateOnly(new Date()), req.query.date?.toString() || common.getISODateOnly(new Date()),
common.parseBool(req.query.copyFromPreviousMonth), common.parseBool(req.query.copyFromPreviousMonth),
common.parseBool(req.query.autoFill), common.parseBool(req.query.autoFill),
common.parseBool(req.query.forDay)); common.parseBool(req.query.forDay),
parseInt(req.query.type) || 0,
);
res.send(JSON.stringify(result?.error?.toString())); res.send(JSON.stringify(result?.error?.toString()));
break; break;
case "delete": case "delete":
@ -465,7 +469,29 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// ### COPIED TO shift api (++) ### // ### COPIED TO shift api (++) ###
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) {
let scheduledPubsPerDayAndWeek = {};
// Function to flatten the registry
// Function to update the registry
function updateRegistry(publisherId, day, weekNr) {
// Registry schema: {day: {weekNr: [publisherIds]}}
const dayKey = common.getISODateOnly(day);
if (!scheduledPubsPerDayAndWeek[dayKey]) {
scheduledPubsPerDayAndWeek[dayKey] = {};
}
if (!scheduledPubsPerDayAndWeek[dayKey][weekNr]) {
scheduledPubsPerDayAndWeek[dayKey][weekNr] = [];
}
scheduledPubsPerDayAndWeek[dayKey][weekNr].push(publisherId);
}
function flattenRegistry(dayKey) {
const weekEntries = scheduledPubsPerDayAndWeek[dayKey] || {};
return Object.values(weekEntries).flat();
}
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0) {
let missingPublishers = []; let missingPublishers = [];
let publishersWithChangedPref = []; let publishersWithChangedPref = [];
@ -494,6 +520,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let dayNr = 1; let dayNr = 1;
let weekNr = 1; let weekNr = 1;
if (forDay) { if (forDay) {
day = monthInfo.date; day = monthInfo.date;
endDate.setDate(monthInfo.date.getDate() + 1); endDate.setDate(monthInfo.date.getDate() + 1);
@ -503,6 +530,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let publishersThisWeek = []; let publishersThisWeek = [];
// 0. generate shifts and assign publishers from the previous month if still available // 0. generate shifts and assign publishers from the previous month if still available
while (day < endDate) { while (day < endDate) {
let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false); let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false);
@ -542,8 +571,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
if (shiftLastMonthSameDay) { if (shiftLastMonthSameDay) {
for (let assignment of shiftLastMonthSameDay.assignments) { for (let assignment of shiftLastMonthSameDay.assignments) {
let publisher = assignment.publisher; let publisher = assignment.originalPublisher ?? assignment.publisher;
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName); console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName + assignment.originalPublisher ? " (original)" : "");
let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr); let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr);
console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime)); console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime));
@ -554,28 +583,17 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut
}); });
publishersThisWeek.push(publisher.id); publishersThisWeek.push(publisher.id);
updateRegistry(publisher.id, day, weekNr);
} }
} }
} }
let publishersNeeded = event.numberOfPublishers - shiftAssignments.length; let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
//ToDo: check if getAvailablePublishersForShift is working correctly. It seems not to! //ToDo: check if getAvailablePublishersForShift is working correctly. It seems not to!
let availablePublishers = await getAvailablePublishersForShiftNew(shiftStart, shiftEnd, availablePubsForTheDay, publishersThisWeek); let availablePublishers = await getAvailablePublishersForShiftNew(shiftStart, shiftEnd, availablePubsForTheDay, publishersThisWeek);
console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length); console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length);
// Prioritize publishers with minimal availability
// SKIP ADDING PUBLISHERS FOR NOW
// availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
// for (let i = 0; i < publishersNeeded; i++) {
// if (availablePublishers[i]) {
// shiftAssignments.push({ publisherId: availablePublishers[i].id });
// publishersThisWeek.push(availablePublishers[i].id);
// }
// }
const createdShift = await prisma.shift.create({ const createdShift = await prisma.shift.create({
data: { data: {
startTime: shiftStart, startTime: shiftStart,
@ -633,8 +651,9 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
}); });
let publishersToday = []; let publishersToday = [];
let rankedPublishers = [];
// 2. First pass - prioritize shifts with transport where it is needed // Second pass - prioritize shifts with transport where it is needed
console.log(" second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year); console.log(" second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year);
day = new Date(monthInfo.firstMonday); day = new Date(monthInfo.firstMonday);
dayNr = 1; dayNr = 1;
@ -646,8 +665,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false); let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false);
let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day)); let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day));
//let publishersToday = shifts.flatMap(s => s.assignments.map(a => a.publisher.id));
//get all publishers assigned for the day from the database
let publishersToday = await prisma.assignment.findMany({ let publishersToday = await prisma.assignment.findMany({
where: { where: {
shift: { shift: {
@ -662,7 +679,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
}, },
}).then((assignments) => assignments.map(a => a.publisherId)); }).then((assignments) => assignments.map(a => a.publisherId));
let transportShifts = shifts.filter(s => s.requiresTransport); let transportShifts = shifts.filter(s => s.requiresTransport);
transportShifts[0].transportIn = true; transportShifts[0].transportIn = true;
if (transportShifts.length > 1) { if (transportShifts.length > 1) {
@ -671,7 +687,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
// if there are no transport yet: // if there are no transport yet:
// transportShifts.forEach(async shift => { // transportShifts.forEach(async shift => {
for (const shift of transportShifts) { for (const shift of transportShifts) {
//todo: replace that with transport check
let publishersNeeded = event.numberOfPublishers - shift.assignments.length; let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
let transportCapable = shift.assignments.filter(a => a.isWithTransport); let transportCapable = shift.assignments.filter(a => a.isWithTransport);
let tramsportCapableMen = transportCapable.filter(a => a.publisher.isMale); let tramsportCapableMen = transportCapable.filter(a => a.publisher.isMale);
@ -691,10 +706,13 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
return (hasTransportInAvailability || hasTransportOutAvailability); return (hasTransportInAvailability || hasTransportOutAvailability);
}); });
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift); availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
// rank publishers based on different factors if (algType == 0) {
let rankedPublishersOld = await RankPublishersForShift([...availablePublishers]) rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
let rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers]) } else if (algType == 1) {
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
}
if (rankedPublishers.length > 0) { if (rankedPublishers.length > 0) {
const newAssignment = await prisma.assignment.create({ const newAssignment = await prisma.assignment.create({
data: { data: {
@ -716,8 +734,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
shift.assignments.push(newAssignment); shift.assignments.push(newAssignment);
publishersToday.push(rankedPublishers[0].id); publishersToday.push(rankedPublishers[0].id);
updateRegistry(rankedPublishers[0].id, day, weekNr);
} }
} }
} }
} }
@ -725,10 +743,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
day.setDate(day.getDate() + 1); day.setDate(day.getDate() + 1);
} }
// Fill the rest of the shifts
let goal = 1;
// 3. next passes - fill the rest of the shifts
let goal = 1; // 4 to temporary skip
while (goal <= 4) { while (goal <= 4) {
console.log("#".repeat(50)); console.log("#".repeat(50));
console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year); console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year);
@ -741,7 +757,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
if (event) { if (event) {
let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false); let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false);
let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day)); let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day));
// let publishersToday = shifts.flatMap(s => s.assignments.map(a => a.publisher?.id));
let publishersToday = await prisma.assignment.findMany({ let publishersToday = await prisma.assignment.findMany({
where: { where: {
shift: { shift: {
@ -760,14 +775,18 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers"); console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers");
for (const shift of shiftsToFill) { for (const shift of shiftsToFill) {
console.log("Filling shift " + shift.name + " with " + goal + " publishers");
let publishersNeeded = event.numberOfPublishers - shift.assignments.length; let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
if (publishersNeeded > 0 && shift.assignments.length < goal) { if (publishersNeeded > 0 && shift.assignments.length < goal) {
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', shift.startTime, true, false, false, true, false); let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', shift.startTime, true, false, false, true, false);
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift); let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
if (algType == 0) {
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
} else if (algType == 1) {
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
}
shift.availablePublishers = availablePublishers.length;
let rankedPublishers = await RankPublishersForShift([...availablePublishers])
if (rankedPublishers.length == 0) { if (rankedPublishers.length == 0) {
console.log("No available publishers for shift " + shift.name); console.log("No available publishers for shift " + shift.name);
} else if (rankedPublishers.length > 0) { } else if (rankedPublishers.length > 0) {
@ -790,8 +809,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
}); });
shift.assignments.push(newAssignment); shift.assignments.push(newAssignment);
publishersToday.push(rankedPublishers[0].id); publishersToday.push(rankedPublishers[0].id);
updateRegistry(rankedPublishers[0].id, day, weekNr);
//check if publisher.familyMembers are also available and add them to the shift. ToDo: test case
let familyMembers = availablePubsForTheShift.filter(p => p.familyHeadId && p.familyHeadId === rankedPublishers[0].familyHeadId); let familyMembers = availablePubsForTheShift.filter(p => p.familyHeadId && p.familyHeadId === rankedPublishers[0].familyHeadId);
if (familyMembers.length > 0) { if (familyMembers.length > 0) {
familyMembers.forEach(async familyMember => { familyMembers.forEach(async familyMember => {
@ -815,18 +834,18 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
}); });
shift.assignments.push(newAssignment); shift.assignments.push(newAssignment);
publishersToday.push(familyMember.id); publishersToday.push(familyMember.id);
updateRegistry(familyMember.id, day, weekNr);
} }
}); });
} }
} }
} }
}; }
} }
day.setDate(day.getDate() + 1); day.setDate(day.getDate() + 1);
} }
goal += 1 goal += 1;
} }
if (!forDay) { if (!forDay) {
@ -845,11 +864,9 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) { async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) {
//ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later. //ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later.
let goodPublishers = availablePublishers.filter(p => { let goodPublishers = availablePublishers.filter(p => {
const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id); const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id);
const isNotAssignedToday = !pubsToExclude.includes(p.id); const isNotAssignedToday = !pubsToExclude.includes(p.id);
const isAssignedEnough = p.currentMonthAssignments >= p.desiredShiftsPerMonth; const isAssignedEnough = p.currentMonthAssignments >= p.desiredShiftsPerMonth;
//if (isAssignedEnough) console.log(p.firstName + " " + p.lastName + " is assigned enough: " + p.currentMonthAssignments + " >= " + p.desiredShiftsPerMonth);
return isNotAssigned && isNotAssignedToday && !isAssignedEnough; return isNotAssigned && isNotAssignedToday && !isAssignedEnough;
}); });
return goodPublishers; return goodPublishers;
@ -865,7 +882,7 @@ async function FilterInappropriatePublishers(availablePublishers, pubsToExclude,
// 6. Idealy noone should be more than once a week. disqualify publishers already on a shift this week. only assign them if there are no other options and we have less than 3 publishers on a specific shift. // 6. Idealy noone should be more than once a week. disqualify publishers already on a shift this week. only assign them if there are no other options and we have less than 3 publishers on a specific shift.
//sort publishers to rank the best option for the current shift assignment //sort publishers to rank the best option for the current shift assignment
async function RankPublishersForShift(publishers) { async function RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek, currentDay) {
publishers.forEach(p => { publishers.forEach(p => {
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments; p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
}); });
@ -874,22 +891,41 @@ async function RankPublishersForShift(publishers) {
// males first (descending) // males first (descending)
if (a.isMale && !b.isMale) return -1; if (a.isMale && !b.isMale) return -1;
// desired completion (normalized 0%=0 - 100%=1) ; lower first // desired completion (normalized 0%=0 - 100%=1); lower first
const desiredCompletion = a.currentMonthAssignments / a.desiredShiftsPerMonth - b.currentMonthAssignments / b.desiredShiftsPerMonth;
//console.log(a.firstName + " " + a.lastName + " desiredCompletion: " + desiredCompletion, a.currentMonthAssignments, "/", a.desiredShiftsPerMonth);
if (desiredCompletion !== 0) return desiredCompletion;
// const desiredDifference = b.DesiredMinusCurrent - a.DesiredMinusCurrent;
// if (desiredDifference !== 0) return desiredDifference;
const desiredCompletionA = a.currentMonthAssignments / a.desiredShiftsPerMonth; const desiredCompletionA = a.currentMonthAssignments / a.desiredShiftsPerMonth;
const desiredCompletionB = b.currentMonthAssignments / b.desiredShiftsPerMonth; const desiredCompletionB = b.currentMonthAssignments / b.desiredShiftsPerMonth;
console.log(`${a.firstName} ${a.lastName} desiredCompletion: ${desiredCompletionA}, ${a.currentMonthAssignments} / ${a.desiredShiftsPerMonth}`);
// console.log(`${b.firstName} ${b.lastName} desiredCompletion: ${desiredCompletionB}, ${b.currentMonthAssignments} / ${b.desiredShiftsPerMonth}`); let adjustedCompletionA = desiredCompletionA;
let adjustedCompletionB = desiredCompletionB;
// Apply penalties based on proximity to current day
for (let i = 1; i <= 6; i++) {
const previousDayKey = common.getISODateOnly(addDays(currentDay, -i));
const nextDayKey = common.getISODateOnly(addDays(currentDay, i));
const penalty = [0.5, 0.7, 0.8, 0.85, 0.9, 0.95][i - 1]; // Penalties for +-1 to +-6 days
if (flattenRegistry(previousDayKey).includes(a.id)) {
adjustedCompletionA *= penalty;
}
if (flattenRegistry(previousDayKey).includes(b.id)) {
adjustedCompletionB *= penalty;
}
if (flattenRegistry(nextDayKey).includes(a.id)) {
adjustedCompletionA *= penalty;
}
if (flattenRegistry(nextDayKey).includes(b.id)) {
adjustedCompletionB *= penalty;
}
}
const desiredCompletionDiff = adjustedCompletionA - adjustedCompletionB;
if (desiredCompletionDiff !== 0) return desiredCompletionDiff;
// less available first (ascending) // less available first (ascending)
const availabilityDifference = a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount; const availabilityDifference = a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
if (availabilityDifference !== 0) return availabilityDifference; if (availabilityDifference !== 0) return availabilityDifference;
// less assigned first (ascending) // less assigned first (ascending)
return a.currentMonthAssignments - b.currentMonthAssignments; return a.currentMonthAssignments - b.currentMonthAssignments;
}); });
@ -897,8 +933,9 @@ async function RankPublishersForShift(publishers) {
return ranked; return ranked;
} }
// ToDo: add negative weights for currentweekAssignments, so we avoid assigning the same publishers multiple times in a week. having in mind the days difference between shifts. // ToDo: add negative weights for currentweekAssignments, so we avoid assigning the same publishers multiple times in a week. having in mind the days difference between shifts.
async function RankPublishersForShiftWeighted(publishers) { async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
// Define weights for each criterion // Define weights for each criterion
const weights = { const weights = {
gender: 2, gender: 2,
@ -919,23 +956,51 @@ async function RankPublishersForShiftWeighted(publishers) {
p.desiredCompletion = p.currentMonthAssignments / p.desiredShiftsPerMonth; p.desiredCompletion = p.currentMonthAssignments / p.desiredShiftsPerMonth;
}); });
let ranked = publishers.sort((a, b) => { const calculateScoreAndPenalties = (p) => {
// Calculate weighted score for each publisher let score = (p.isMale ? weights.gender : 0) -
const scoreA = (a.isMale ? weights.gender : 0) - (p.desiredCompletion * weights.desiredCompletion) +
(a.desiredCompletion * weights.desiredCompletion) + ((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
((1 - a.currentMonthAvailabilityHoursCount / 24) * weights.availability) + (p.currentMonthAssignments * weights.lastMonthCompletion) -
(a.currentMonthAssignments * weights.lastMonthCompletion) - (p.currentMonthAssignments * weights.currentAssignments);
(a.currentMonthAssignments * weights.currentAssignments);
const scoreB = (b.isMale ? weights.gender : 0) - let penalties = [];
(b.desiredCompletion * weights.desiredCompletion) +
((1 - b.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(b.currentMonthAssignments * weights.lastMonthCompletion) -
(b.currentMonthAssignments * weights.currentAssignments);
return scoreB - scoreA; // Sort descending by score // Apply penalties based on proximity to current day
for (let i = 1; i <= 6; i++) {
const previousDayKey = common.getISODateOnly(addDays(currentDay, -i));
const nextDayKey = common.getISODateOnly(addDays(currentDay, i));
const penalty = [0.5, 0.7, 0.8, 0.85, 0.9, 0.95][i - 1]; // Penalties for +-1 to +-6 days
if (flattenRegistry(previousDayKey).includes(p.id)) {
score *= penalty;
penalties.push({ day: previousDayKey, penalty });
}
if (flattenRegistry(nextDayKey).includes(p.id)) {
score *= penalty;
penalties.push({ day: nextDayKey, penalty });
}
}
return { score, penalties };
};
// Calculate scores and penalties for each publisher
publishers.forEach(p => {
const result = calculateScoreAndPenalties(p);
p.score = result.score;
p.penalties = result.penalties;
}); });
// Sort publishers based on score
let ranked = publishers.sort((a, b) => b.score - a.score);
// Log the scores and penalties of the top publisher
if (ranked.length > 0) {
console.log(`Top Publisher: ${ranked[0].firstName} ${ranked[0].lastName}`,
` Score: ${ranked[0].score}`, "last score: ", ranked[ranked.length - 1].score,
` Penalties: `, ranked[0].penalties);
}
return ranked; return ranked;
} }
@ -943,6 +1008,7 @@ async function RankPublishersForShiftWeighted(publishers) {
async function DeleteShiftsForMonth(monthInfo) { async function DeleteShiftsForMonth(monthInfo) {
try { try {
const prisma = common.getPrismaClient(); const prisma = common.getPrismaClient();

View File

@ -358,16 +358,16 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const addAssignment = async (publisher, shiftId) => { const addAssignment = async (publisher, shiftId) => {
try { try {
console.log(`new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`); console.log(`calendar.idx: new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
const newAssignment = { const newAssignment = {
publisher: { connect: { id: publisher.id } }, publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } }, shift: { connect: { id: shiftId } },
isActive: true,
isConfirmed: true isConfirmed: true
}; };
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment); const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
// Update the 'publisher' property of the returned data with the full publisher object // Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher; data.publisher = publisher;
handleAssignmentChange(publisher.id, 'add');
} catch (error) { } catch (error) {
console.error("Error adding assignment:", error); console.error("Error adding assignment:", error);
} }
@ -375,12 +375,25 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const removeAssignment = async (publisher, shiftId) => { const removeAssignment = async (publisher, shiftId) => {
try { try {
const assignment = publisher.assignments.find(ass => ass.shift.id === shiftId); const assignment = publisher.assignments.find(ass => ass.shift.id === shiftId);
console.log(`remove assignment for shift ${shiftId}`); console.log(`calendar.idx: remove assignment for shift ${shiftId}`);
const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`); const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`);
//remove from local assignments:
publisher.assignments = publisher.assignments.filter(a => a.id !== assignment.id)
//
handleAssignmentChange(publisher.id, 'remove')
} catch (error) { } catch (error) {
console.error("Error removing assignment:", error); console.error("Error removing assignment:", error);
} }
} }
function handleAssignmentChange(id, type) {
// Handle assignment change logic here
let pub = availablePubs.find(pub => pub.id === id)
pub.currentMonthAssignments += type === 'add' ? 1 : -1;
pub.currentWeekAssignments += type === 'add' ? 1 : -1;
//store in state
setAvailablePubs([...availablePubs]);
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// button handlers // button handlers
@ -421,10 +434,10 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
setActiveButton(null); setActiveButton(null);
} }
} }
const generateShifts = async (buttonId, copyFromPrevious = false, autoFill = false, forDay?: Boolean | null) => { const generateShifts = async (buttonId, copyFromPrevious = false, autoFill = false, forDay?: Boolean | null, type = 0) => {
try { try {
setActiveButton(buttonId); setActiveButton(buttonId);
const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}&copyFromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}`; const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}&copyFromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}&type=${type}`;
const { shifts } = await axiosInstance.get(endpoint); const { shifts } = await axiosInstance.get(endpoint);
toast.success('Готово!', { autoClose: 1000 }); toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false); setIsMenuOpen(false);
@ -559,6 +572,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`); await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
} }
async function handleCreateNewShift(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> { async function handleCreateNewShift(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
//get last shift end time //get last shift end time
let lastShift = shifts.sort((a, b) => new Date(b.endTime).getTime() - new Date(a.endTime).getTime())[0]; let lastShift = shifts.sort((a, b) => new Date(b.endTime).getTime() - new Date(a.endTime).getTime())[0];
@ -678,9 +692,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap" onClick={() => generateShifts("genCopy", true)}> <button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap" onClick={() => generateShifts("genCopy", true)}>
{isLoading('genCopy') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-copy mr-2"></i>)} {isLoading('genCopy') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-copy mr-2"></i>)}
копирай от миналия месец</button> копирай от миналия месец</button>
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true)}> <button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 0)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)} {isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени </button> Генерирай смени </button>
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 1)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 2 </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" <button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100"
onClick={() => openConfirmModal( onClick={() => openConfirmModal(
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?', 'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
@ -805,12 +822,19 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
{/* Shift list section */} {/* Shift list section */}
<div className="flex-grow mx-5"> <div className="flex-grow mx-5">
<div className="flex-col" id="shiftlist"> <div className="flex-col" id="shiftlist">
{shifts.map((shift, index) => ( {shifts.map((shift, index) => (
<ShiftComponent key={index} shift={shift} <ShiftComponent
onShiftSelect={handleShiftSelection} isSelected={shift.id == selectedShiftId} key={index}
onPublisherSelect={handleSelectedPublisher} showAllAuto={true} shift={shift}
allPublishersInfo={availablePubs} /> onShiftSelect={handleShiftSelection}
isSelected={shift.id == selectedShiftId}
onPublisherSelect={handleSelectedPublisher}
onAssignmentChange={handleAssignmentChange}
showAllAuto={true}
allPublishersInfo={availablePubs}
/>
))} ))}
</div> </div>
<button <button
@ -842,7 +866,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value); return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
}); });
const dayShifts = weekShifts.map(shift => { const dayShifts = weekShifts.map(shift => {
const isAvailable = publisher.availabilities.some(avail => const isAvailable = publisher.availabilities?.some(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
); );
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300'; let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
@ -863,8 +887,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const hasAssignment = (shiftId) => { const hasAssignment = (shiftId) => {
// return publisher.assignments.some(ass => ass.shift.id == shiftId); // return publisher.assignments.some(ass => ass.shift.id == shiftId);
return publisher.assignments.some(ass => { return publisher.assignments?.some(ass => {
console.log(`Comparing: ${ass.shift.id} to ${shiftId}: ${ass.shift.id === shiftId}`); //console.log(`Comparing: ${ass.shift.id} to ${shiftId}: ${ass.shift.id === shiftId}`);
return ass.shift.id === shiftId; return ass.shift.id === shiftId;
}); });
}; };
@ -911,7 +935,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
return ( return (
<div <div
key={index} key={index}
className={`text-sm text-white p-2 rounded-md ${isFromPrevMonth ? 'border-l-6 border-black-500' : ''} ${assignmentExists ? 'bg-blue-200' : shift.color} h-24 flex flex-col justify-center`} className={`text-sm text-white p-2 rounded-md ${isFromPrevMonth ? 'border-l-6 border-black-500' : ''} ${shift.color} ${assignmentExists ? 'border-2 border-blue-500' : ""} h-24 flex flex-col justify-center`}
> >
{common.getTimeRange(shift.startTime, shift.endTime)} {shift.id} {common.getTimeRange(shift.startTime, shift.endTime)} {shift.id}

View File

@ -35,9 +35,9 @@ const SchedulePage = () => {
}, []); // Empty dependency array means this effect runs once on component mount }, []); // Empty dependency array means this effect runs once on component mount
// temporary alert for the users // temporary alert for the users
useEffect(() => { // useEffect(() => {
alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'"); // alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
}, []); // }, []);
return ( return (

View File

@ -28,9 +28,9 @@ export default function MySchedulePage({ assignments }) {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
// temporary alert for the users // temporary alert for the users
useEffect(() => { // useEffect(() => {
alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'"); // alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
}, []); // }, []);
if (status === "loading") { if (status === "loading") {
return <div className="flex justify-center items-center h-screen">Зареждане...</div>; return <div className="flex justify-center items-center h-screen">Зареждане...</div>;

View File

@ -31,7 +31,7 @@ export default function DashboardPage({ initialItems, initialUserId, cartEvents,
const router = useRouter(); const router = useRouter();
const { newLogin } = router.query; const { newLogin } = router.query;
const { data: session } = useSession(); const { data: session } = useSession();
const [userName, setUserName] = useState(session?.user?.name); const [userName, setUserName] = useState('');
const [userId, setUserId] = useState(initialUserId); const [userId, setUserId] = useState(initialUserId);
const [events, setEvents] = useState(initialItems?.map(item => ({ const [events, setEvents] = useState(initialItems?.map(item => ({
...item, ...item,
@ -43,7 +43,7 @@ export default function DashboardPage({ initialItems, initialUserId, cartEvents,
}))); })));
useEffect(() => { useEffect(() => {
if (session) { if (session && userName === '' && session.user.name) {
setUserName(session.user.name); setUserName(session.user.name);
setUserId(session.user.id); setUserId(session.user.id);
//handleUserSelection({ id: session.user.id, firstName: session.user.name, lastName: '' }); //handleUserSelection({ id: session.user.id, firstName: session.user.name, lastName: '' });
@ -56,7 +56,7 @@ export default function DashboardPage({ initialItems, initialUserId, cartEvents,
useEffect(() => { useEffect(() => {
//if (newLogin === 'true') //if (newLogin === 'true')
{ {
alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'"); // alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
const currentPath = router.pathname; const currentPath = router.pathname;
router.replace(currentPath, undefined, { shallow: true }); // Removes the query without affecting the history router.replace(currentPath, undefined, { shallow: true }); // Removes the query without affecting the history
} }
@ -281,6 +281,7 @@ export const getServerSideProps = async (context) => {
endTime: 'desc' endTime: 'desc'
} }
})).endTime; })).endTime;
return { return {
props: { props: {
initialItems: items, initialItems: items,

View File

@ -110,6 +110,9 @@ Date.prototype.getDayEuropean = function () {
return (day === 0) ? 6 : day - 1; // Convert 0 (Sunday) to 6, and decrement other days by 1 return (day === 0) ? 6 : day - 1; // Convert 0 (Sunday) to 6, and decrement other days by 1
}; };
exports.getDayOfWeek = function (date) {
return date.getDayEuropean();
};
// Helper function to convert month name to 0-based index // Helper function to convert month name to 0-based index
exports.getMonthNames = function () { exports.getMonthNames = function () {
return ["януари", "февруари", "март", "април", "май", "юни", "юли", "август", "септември", "октомври", "ноември", "декември"]; return ["януари", "февруари", "март", "април", "май", "юни", "юли", "август", "септември", "октомври", "ноември", "декември"];
@ -374,7 +377,10 @@ exports.getDateFormattedShort = function (date) {
} }
exports.getDateTimeFormatted = function (date) {
// const startTime = start.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });
return exports.getISODateOnly(new Date(date)) + " " + exports.getTimeFormatted(date);
}
/*Todo: remove: /*Todo: remove:
toISOString toISOString

View File

@ -210,9 +210,10 @@ async function getAvailabilities(userId) {
* Filters publishers based on various criteria including exact times, monthly duration, * Filters publishers based on various criteria including exact times, monthly duration,
* and whether or not to include statistics about publishers' availabilities and assignments. * and whether or not to include statistics about publishers' availabilities and assignments.
* This function heavily relies on the `prisma` client to query and manipulate data related to publishers. * This function heavily relies on the `prisma` client to query and manipulate data related to publishers.
* (ToDo: implement separate and simple fns if it does not work)
* *
* @param {Array|string} selectFields - Fields to select from the publishers data. Can be an array of field names or a comma-separated string of field names. * @param {Array|string} selectFields - Fields to select from the publishers data. Can be an array of field names or a comma-separated string of field names.
* @param {string|Date} filterDate - The reference date for filtering. Can be a date string or a Date object. Used to determine relevant time frames like current month, previous month, etc. * @param {string|Date|null} filterDate - The reference date for filtering. Can be a date string, a Date object, or null. Used to determine relevant time frames like current month, previous month, etc.
* @param {boolean} [isExactTime=false] - If true, filters publishers who are available at the exact time of `filterDate` plus/minus a specific duration (e.g., 90 minutes). * @param {boolean} [isExactTime=false] - If true, filters publishers who are available at the exact time of `filterDate` plus/minus a specific duration (e.g., 90 minutes).
* @param {boolean} [isForTheMonth=false] - If true, adjusts the filtering to encompass the entire month based on `filterDate`. * @param {boolean} [isForTheMonth=false] - If true, adjusts the filtering to encompass the entire month based on `filterDate`.
* @param {boolean} [noEndDateFilter=false] - If true, removes any filtering based on the end date of publishers' availabilities. * @param {boolean} [noEndDateFilter=false] - If true, removes any filtering based on the end date of publishers' availabilities.
@ -221,10 +222,40 @@ async function getAvailabilities(userId) {
* *
* @returns {Promise<Array>} Returns a promise that resolves to an array of publishers with filtered data according to the specified criteria. * @returns {Promise<Array>} Returns a promise that resolves to an array of publishers with filtered data according to the specified criteria.
*/ */
async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false) { // (ToDo: implement separate and simple fns if it does not work)
async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false, id = null, filterAvailabilitiesByDate = true) {
const prisma = common.getPrismaClient(); const prisma = common.getPrismaClient();
filterDate = new Date(filterDate); // Convert to date object if not already
let filterTimeFrom, filterTimeTo;
if (filterDate === null || filterDate === "" || filterDate === undefined) {
noEndDateFilter = true;
isForTheMonth = false;
isExactTime = false;
} else {
filterDate = new Date(filterDate); // Convert to date object if not already
//check if filterDate is valid date
if (isNaN(filterDate.getTime())) {
console.error("Invalid date: " + filterDate);
filterTimeFrom = new Date(2024, 1, 1);
noEndDateFilter = true;
isForTheMonth = false
isExactTime = false;
} else {
filterTimeFrom = new Date(filterDate)
filterTimeTo = new Date(filterDate);
if (isExactTime) {
//add +- 90 minutes to the filterDate ToDo: should be "shift duration"
// filterTimeFrom.setMinutes(filterTimeFrom.getMinutes() - 90);
filterTimeTo.setMinutes(filterTimeTo.getMinutes() + 90);
}
else {
filterTimeFrom.setHours(0, 0, 0, 0);
filterTimeTo.setHours(23, 59, 59, 999);
}
}
}
const dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterTimeFrom);
const monthInfo = common.getMonthDatesInfo(filterDate); const monthInfo = common.getMonthDatesInfo(filterDate);
let prevMnt = new Date(filterDate) let prevMnt = new Date(filterDate)
@ -239,7 +270,6 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
return acc; return acc;
}, {}); }, {});
selectBase.assignments = { selectBase.assignments = {
select: { select: {
id: true, id: true,
@ -251,20 +281,21 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
} }
} }
}, },
where: { // where: {
shift: { // shift: {
startTime: { // startTime: {
gte: prevMnt, // gte: prevMnt,
} // }
} // }
} // }
}; };
let filterTimeFrom = new Date(filterDate)
let filterTimeTo = new Date(filterDate);
let isDayFilter = true; let isDayFilter = true;
let whereClause = {}; let whereClause = {};
if (id) {
whereClause.id = String(id)
}
if (isForTheMonth) { if (isForTheMonth) {
var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber
@ -272,69 +303,76 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
filterTimeTo = monthInfo.lastSunday; filterTimeTo = monthInfo.lastSunday;
isDayFilter = false; isDayFilter = false;
} }
if (isExactTime) {
//add +- 90 minutes to the filterDate ToDo: should be "shift duration"
// filterTimeFrom.setMinutes(filterTimeFrom.getMinutes() - 90);
filterTimeTo.setMinutes(filterTimeTo.getMinutes() + 90);
}
else {
filterTimeFrom.setHours(0, 0, 0, 0);
filterTimeTo.setHours(23, 59, 59, 999);
}
whereClause["availabilities"] = {
some: {
OR: [
// Check if dayOfMonth is not null and startTime is after monthInfo.firstMonday (Assignments on specific days AND time)
{
//dayOfMonth: { not: null },
startTime: { gte: filterTimeFrom },
// endTime: { lte: monthInfo.lastSunday }
},
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
{
// dayOfMonth: null,
// startTime: { gte: filterTimeFrom },
AND: [
{ dayOfMonth: null },
{ startTime: { lte: filterTimeTo } }, // startTime included
{
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
{ endDate: { gte: filterTimeFrom } }, // endDate included
{ endDate: null }
]
}
]
}
]
}
};
/* FILTERS if (filterTimeFrom) { // if no date is provided, we don't filter by date
1. exact time
2. exact date
3. the month
4. from start date only
*/
if (noEndDateFilter) { selectBase.assignments.where = {
isDayFilter = false; shift: {
} startTime: { gte: prevMnt },
else { }
whereClause["availabilities"].some.OR[0].endTime = { lte: filterTimeTo }; };
if (isForTheMonth) { whereClause["availabilities"] = {
// no dayofweek or time filters here some: {
OR: [
// ONE TIME AVAILABILITIES
// Check if dayOfMonth is not null and startTime is after monthInfo.firstMonday (Assignments on specific days AND time)
{
//dayOfMonth: { not: null },
startTime: { gte: filterTimeFrom },
// endTime: { lte: monthInfo.lastSunday }
},
// REPEATING WEEKLY AVAILABILITIES
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
{
// dayOfMonth: null,
// startTime: { gte: filterTimeFrom },
AND: [
{ dayOfMonth: null },
//{ dayofweek: dayOfWeekEnum }, // we want all days of the week for now
// moved down to conditional filters
// { startTime: { lte: filterTimeTo } }, // we ignore startTime as it will be filtered later only by the time and not by date.
{
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
{ endDate: { gte: filterTimeFrom } }, // endDate included
{ endDate: null }
]
}
]
}
]
}
};
/* FILTERS
1. exact time
2. exact date
3. the month
4. from start date only
*/
if (noEndDateFilter) {
isDayFilter = false;
} }
else { else {
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate); whereClause["availabilities"].some.OR[0].endTime = { lte: filterTimeTo };
whereClause["availabilities"].some.OR[1].dayofweek = dayOfWeekEnum; if (isForTheMonth) {
//NOTE: we filter by date after we calculate the correct dates post query // no dayofweek or time filters here
if (isExactTime) {
//if exact time we need the availability to be starting on or before start of the shift and ending on or after the end of the shift
whereClause["availabilities"].some.OR[0].startTime = { lte: filterTimeFrom };
whereClause["availabilities"].some.OR[0].endTime = { gte: filterTimeTo };
} }
else { else {
//moved to upper qery as it is now also dependant on date filter
if ((isDayFilter || filterAvailabilitiesByDate) && !isForTheMonth) {
whereClause["availabilities"].some.OR[1].dayofweek = dayOfWeekEnum;
}
//NOTE: we filter by date after we calculate the correct dates post query
if (isExactTime) {
//if exact time we need the availability to be starting on or before start of the shift and ending on or after the end of the shift
whereClause["availabilities"].some.OR[0].startTime = { lte: filterTimeFrom };
whereClause["availabilities"].some.OR[0].endTime = { gte: filterTimeTo };
}
else {
}
} }
} }
} }
@ -346,7 +384,7 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
where: whereClause, where: whereClause,
select: { select: {
...selectBase, ...selectBase,
availabilities: true availabilities: true // we select all availabilities here and should filter them later
} }
}); });
@ -354,10 +392,17 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
// include repeating weekly availabilities. generate occurrences for the month // include repeating weekly availabilities. generate occurrences for the month
// convert matching weekly availabilities to availabilities for the day to make further processing easier on the client. // convert matching weekly availabilities to availabilities for the day to make further processing easier on the client.
// we trust that the filtering was OK, so we use the dateFilter as date.
publishers.forEach(pub => { publishers.forEach(pub => {
pub.availabilities = pub.availabilities.map(avail => { pub.availabilities = pub.availabilities.map(avail => {
if (avail.dayOfMonth == null) { if (avail.dayOfMonth == null) {
if (filterAvailabilitiesByDate && !isForTheMonth) {
// filter out repeating availabilities when on other day of week
if (filterTimeFrom) {
if (avail.dayofweek != dayOfWeekEnum) {
return null;
}
}
}
let newStart = new Date(filterDate); let newStart = new Date(filterDate);
newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0); newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
let newEnd = new Date(filterDate); let newEnd = new Date(filterDate);
@ -368,8 +413,17 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
endTime: newEnd endTime: newEnd
} }
} }
return avail; else {
}); if (filterAvailabilitiesByDate && !isForTheMonth) {
if (avail.startTime >= filterTimeFrom && avail.startTime <= filterTimeTo) {
return avail;
}
return null;
}
return avail;
}
})
.filter(avail => avail !== null);
}); });
@ -414,7 +468,7 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
pub.currentMonthAvailability = pub.availabilities?.filter(avail => { pub.currentMonthAvailability = pub.availabilities?.filter(avail => {
// return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= monthInfo.lastSunday; // return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= monthInfo.lastSunday;
return (avail.startTime >= monthInfo.firstMonday && (noEndDateFilter || avail.startTime <= monthInfo.lastSunday)) return (avail.startTime >= monthInfo.firstMonday && (noEndDateFilter || avail.startTime <= monthInfo.lastSunday))
|| (avail.dayOfMonth == null); // include repeating availabilities || (avail.dayOfMonth == null && avail.startTime <= monthInfo.firstMonday); // include repeating availabilities
}) })
pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length; pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length;
// pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => { // pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => {
@ -472,7 +526,7 @@ async function getAllPublishersWithStatisticsMonth(filterDateDuringMonth, noEndD
const monthInfo = common.getMonthDatesInfo(new Date(filterDateDuringMonth)); const monthInfo = common.getMonthDatesInfo(new Date(filterDateDuringMonth));
const dateStr = new Date(monthInfo.firstMonday).toISOString().split('T')[0]; const dateStr = new Date(monthInfo.firstMonday).toISOString().split('T')[0];
let publishers = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', dateStr, false, true, noEndDateFilter, true, true); let publishers = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', dateStr, false, true, noEndDateFilter, true, true, null, 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`);
@ -1179,6 +1233,7 @@ async function FindPublisherAvailability(publisherId, startDate, endDate, dayOfW
// } // }
const fs = require('fs'); const fs = require('fs');
const { filter } = require('jszip');
const path = require('path'); const path = require('path');
async function runSqlFile(filePath) { async function runSqlFile(filePath) {