data.filterPublishers can filter by ID,

api/getPublisherInfo uses dataHelper.filterPubs instead of api.filterPublishers because it handles repeating avs and calcs statistics:
shiftgenerate has fn to rank pubs based on weights.
stop message every page load;
other fixes
This commit is contained in:
Dobromir Popov
2024-05-27 23:50:58 +03:00
parent 870ab6fea4
commit 308c27eba9
7 changed files with 204 additions and 127 deletions

View File

@ -147,9 +147,17 @@ export default async function handler(req, res) {
res.status(200).json(events);
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);
res.status(200).json(pubs[0] || {});
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":
let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day);
res.status(200).json(allpubs);
@ -168,12 +176,6 @@ export default async function handler(req, res) {
//!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName)));
res.status(200).json(publishers);
break;
case "filterPublishersNew":
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
let results = await filterPublishersNew_Available(req.query.select, day,
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities);
res.status(200).json(results);
break;
// find publisher by full name or email
case "findPublisher":

View File

@ -3,6 +3,8 @@
import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt";
import { set, format, addDays } from 'date-fns';
import type { NextApiRequest, NextApiResponse } from "next";
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
@ -465,7 +467,29 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// ### COPIED TO shift api (++) ###
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) {
let missingPublishers = [];
let publishersWithChangedPref = [];
@ -494,6 +518,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let dayNr = 1;
let weekNr = 1;
if (forDay) {
day = monthInfo.date;
endDate.setDate(monthInfo.date.getDate() + 1);
@ -503,6 +528,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let publishersThisWeek = [];
// 0. generate shifts and assign publishers from the previous month if still available
while (day < endDate) {
let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false);
@ -542,8 +569,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
if (shiftLastMonthSameDay) {
for (let assignment of shiftLastMonthSameDay.assignments) {
let publisher = assignment.publisher;
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName);
let publisher = assignment.originalPublisher ?? assignment.publisher;
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName + assignment.originalPublisher ? " (original)" : "");
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));
@ -554,28 +581,17 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut
});
publishersThisWeek.push(publisher.id);
updateRegistry(publisher.id, day, weekNr);
}
}
}
let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
//ToDo: check if getAvailablePublishersForShift is working correctly. It seems not to!
let availablePublishers = await getAvailablePublishersForShiftNew(shiftStart, shiftEnd, availablePubsForTheDay, publishersThisWeek);
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({
data: {
startTime: shiftStart,
@ -634,7 +650,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let publishersToday = [];
// 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);
day = new Date(monthInfo.firstMonday);
dayNr = 1;
@ -646,8 +662,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 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({
where: {
shift: {
@ -662,7 +676,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
},
}).then((assignments) => assignments.map(a => a.publisherId));
let transportShifts = shifts.filter(s => s.requiresTransport);
transportShifts[0].transportIn = true;
if (transportShifts.length > 1) {
@ -671,7 +684,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
// if there are no transport yet:
// transportShifts.forEach(async shift => {
for (const shift of transportShifts) {
//todo: replace that with transport check
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
let transportCapable = shift.assignments.filter(a => a.isWithTransport);
let tramsportCapableMen = transportCapable.filter(a => a.publisher.isMale);
@ -693,8 +705,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
});
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
// rank publishers based on different factors
let rankedPublishersOld = await RankPublishersForShift([...availablePublishers])
let rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers])
// let rankedPublishersOld = await RankPublishersForShift([...availablePublishers])
let rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
if (rankedPublishers.length > 0) {
const newAssignment = await prisma.assignment.create({
data: {
@ -716,8 +728,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
shift.assignments.push(newAssignment);
publishersToday.push(rankedPublishers[0].id);
updateRegistry(rankedPublishers[0].id, day, weekNr);
}
}
}
}
@ -725,10 +737,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
day.setDate(day.getDate() + 1);
}
// 3. next passes - fill the rest of the shifts
let goal = 1; // 4 to temporary skip
// Fill the rest of the shifts
let goal = 1;
while (goal <= 4) {
console.log("#".repeat(50));
console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year);
@ -741,7 +751,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
if (event) {
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 publishersToday = shifts.flatMap(s => s.assignments.map(a => a.publisher?.id));
let publishersToday = await prisma.assignment.findMany({
where: {
shift: {
@ -760,14 +769,15 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers");
for (const shift of shiftsToFill) {
console.log("Filling shift " + shift.name + " with " + goal + " publishers");
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
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 availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
shift.availablePublishers = availablePublishers.length;
let rankedPublishers = await RankPublishersForShift([...availablePublishers])
let rankedPublishers;
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
//rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
if (rankedPublishers.length == 0) {
console.log("No available publishers for shift " + shift.name);
} else if (rankedPublishers.length > 0) {
@ -790,8 +800,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
});
shift.assignments.push(newAssignment);
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);
if (familyMembers.length > 0) {
familyMembers.forEach(async familyMember => {
@ -815,18 +825,18 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
});
shift.assignments.push(newAssignment);
publishersToday.push(familyMember.id);
updateRegistry(familyMember.id, day, weekNr);
}
});
}
}
}
};
}
}
day.setDate(day.getDate() + 1);
}
goal += 1
goal += 1;
}
if (!forDay) {
@ -845,11 +855,9 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) {
//ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later.
let goodPublishers = availablePublishers.filter(p => {
const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id);
const isNotAssignedToday = !pubsToExclude.includes(p.id);
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 goodPublishers;
@ -865,7 +873,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.
//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 => {
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
});
@ -874,22 +882,54 @@ async function RankPublishersForShift(publishers) {
// males first (descending)
if (a.isMale && !b.isMale) return -1;
// 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;
// desired completion (normalized 0%=0 - 100%=1); lower first
const desiredCompletionA = a.currentMonthAssignments / a.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 (scheduledPubsPerDayAndWeek[previousDayKey]?.some(pubId => pubId === a.id)) {
// adjustedCompletionA *= penalty;
// }
// if (scheduledPubsPerDayAndWeek[previousDayKey]?.some(pubId => pubId === b.id)) {
// adjustedCompletionB *= penalty;
// }
// if (scheduledPubsPerDayAndWeek[nextDayKey]?.some(pubId => pubId === a.id)) {
// adjustedCompletionA *= penalty;
// }
// if (scheduledPubsPerDayAndWeek[nextDayKey]?.some(pubId => pubId === b.id)) {
// adjustedCompletionB *= penalty;
// }
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)
const availabilityDifference = a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
if (availabilityDifference !== 0) return availabilityDifference;
// less assigned first (ascending)
return a.currentMonthAssignments - b.currentMonthAssignments;
});
@ -897,8 +937,9 @@ async function RankPublishersForShift(publishers) {
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.
async function RankPublishersForShiftWeighted(publishers) {
async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
// Define weights for each criterion
const weights = {
gender: 2,
@ -919,19 +960,33 @@ async function RankPublishersForShiftWeighted(publishers) {
p.desiredCompletion = p.currentMonthAssignments / p.desiredShiftsPerMonth;
});
let ranked = publishers.sort((a, b) => {
// Calculate weighted score for each publisher
const scoreA = (a.isMale ? weights.gender : 0) -
(a.desiredCompletion * weights.desiredCompletion) +
((1 - a.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(a.currentMonthAssignments * weights.lastMonthCompletion) -
(a.currentMonthAssignments * weights.currentAssignments);
const calculateScore = (p) => {
let score = (p.isMale ? weights.gender : 0) -
(p.desiredCompletion * weights.desiredCompletion) +
((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(p.currentMonthAssignments * weights.lastMonthCompletion) -
(p.currentMonthAssignments * weights.currentAssignments);
const scoreB = (b.isMale ? weights.gender : 0) -
(b.desiredCompletion * weights.desiredCompletion) +
((1 - b.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(b.currentMonthAssignments * weights.lastMonthCompletion) -
(b.currentMonthAssignments * weights.currentAssignments);
// 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;
}
if (flattenRegistry(nextDayKey).includes(p.id)) {
score *= penalty;
}
}
return score;
};
let ranked = publishers.sort((a, b) => {
const scoreA = calculateScore(a);
const scoreB = calculateScore(b);
return scoreB - scoreA; // Sort descending by score
});
@ -943,6 +998,7 @@ async function RankPublishersForShiftWeighted(publishers) {
async function DeleteShiftsForMonth(monthInfo) {
try {
const prisma = common.getPrismaClient();