fix, update and improve shift generation

This commit is contained in:
Dobromir Popov
2024-10-31 21:29:56 +02:00
parent 122a33fa9c
commit b7df2a79d1
2 changed files with 103 additions and 48 deletions

View File

@ -123,25 +123,65 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// ### COPIED TO shift api (++) ###
let scheduledPubsPerDayAndWeek = {};
let publisherMonthlyAssignments = new Map();
let scheduledPubsCacheStatistics = {};
// 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 (!scheduledPubsCacheStatistics[dayKey]) {
scheduledPubsCacheStatistics[dayKey] = {};
}
if (!scheduledPubsPerDayAndWeek[dayKey][weekNr]) {
scheduledPubsPerDayAndWeek[dayKey][weekNr] = [];
if (!scheduledPubsCacheStatistics[dayKey][weekNr]) {
scheduledPubsCacheStatistics[dayKey][weekNr] = [];
}
scheduledPubsPerDayAndWeek[dayKey][weekNr].push(publisherId);
scheduledPubsCacheStatistics[dayKey][weekNr].push(publisherId);
// Update monthly assignments
const currentCount = publisherMonthlyAssignments.get(publisherId) || 0;
publisherMonthlyAssignments.set(publisherId, currentCount + 1);
}
function flattenRegistry(dayKey) {
const weekEntries = scheduledPubsPerDayAndWeek[dayKey] || {};
const weekEntries = scheduledPubsCacheStatistics[dayKey] || {};
return Object.values(weekEntries).flat();
}
// Function to initialize monthly counts from database
async function initializeMonthlyAssignments(prisma, startOfMonth, endOfMonth) {
const monthlyAssignments = await prisma.assignment.groupBy({
by: ['publisherId'],
where: {
shift: {
startTime: {
gte: startOfMonth,
lte: endOfMonth
}
}
},
_count: {
publisherId: true
}
});
publisherMonthlyAssignments = new Map(
monthlyAssignments.map(count => [
count.publisherId,
count._count.publisherId
])
);
}
// Function to get current assignment count for a publisher
function getPublisherAssignmentCount(publisherId) {
return publisherMonthlyAssignments.get(publisherId) || 0;
}
// Function to update publishers with their current counts
function updatePublishersWithCurrentCounts(publishers) {
return publishers.map(pub => ({
...pub,
currentMonthAssignments: getPublisherAssignmentCount(pub.id)
}));
}
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0, until) {
@ -167,7 +207,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
let publishers = await data.getAllPublishersWithStatisticsMonth(date, false, false);
let shiftAssignments = [];
let shiftAssignments: any[] = [];
let day = new Date(monthInfo.firstMonday);
let endDate = monthInfo.lastSunday;
let dayNr = 1;
@ -188,7 +228,12 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
weekNr = common.getWeekNumber(monthInfo.date);
}
let publishersThisWeek = [];
/**
* An array to store the publishers for the current week.
*
* @type {never[]}
*/
let publishersThisWeek: never[] = [];
// # # # # # # # # # # #
@ -242,25 +287,25 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
//---------------------------------------------------
// // COMMENT TO DISABLE COPY FROM LAST MONTH
if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
const transportCount = shiftAssignments.filter(a => a.isWithTransport).length;
const isWithTransport = availability.isWithTransportIn || availability.isWithTransportOut;
// if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
// const transportCount = shiftAssignments.filter(a => a.isWithTransport).length;
// const isWithTransport = availability.isWithTransportIn || availability.isWithTransportOut;
if (!isWithTransport || transportCount < 2) {
shiftAssignments.push({
publisherId: publisher.id,
isConfirmed: true,
isBySystem: true,
isWithTransport: isWithTransport
});
publishersThisWeek.push(publisher.id);
updateRegistry(publisher.id, day, weekNr);
publisher.currentMonthAssignments += 1;
}
else {
console.log(" " + publisher.firstName + " " + publisher.lastName + " skipped (transport already assigned)");
}
}
// if (!isWithTransport || transportCount < 2) {
// shiftAssignments.push({
// publisherId: publisher.id,
// isConfirmed: true,
// isBySystem: true,
// isWithTransport: isWithTransport
// });
// publishersThisWeek.push(publisher.id);
// updateRegistry(publisher.id, day, weekNr);
// publisher.currentMonthAssignments += 1;
// }
// else {
// console.log(" " + publisher.firstName + " " + publisher.lastName + " skipped (transport already assigned)");
// }
// }
//---------------------------------------------------
}
@ -359,7 +404,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
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 publishersToday = await prisma.assignment.findMany({
@ -395,20 +440,22 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
else if (publishersNeeded > 0) {
console.log("shift " + shift.name + " requires transport (" + transportCapable.length + " transport capable)");
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
const availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
const availablePubsWithCounts = updatePublishersWithCurrentCounts(availablePubsForTheShift);
let availablePublishers = availablePubsForTheShift.filter(p => {
const availablePublishers = availablePubsWithCounts.filter(p => {
const hasTransportInAvailability = shift.transportIn && p.availabilities.some(avail => avail.isWithTransportIn);
const hasTransportOutAvailability = shift.transportOut && p.availabilities.some(avail => avail.isWithTransportOut);
return (hasTransportInAvailability || hasTransportOutAvailability);
});
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
const appropriatePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift, 10);
if (algType == 0) {
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
rankedPublishers = await RankPublishersForShiftOld([...appropriatePublishers], scheduledPubsCacheStatistics, day);
} else if (algType == 1) {
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
rankedPublishers = await RankPublishersForShiftWeighted([...appropriatePublishers], scheduledPubsCacheStatistics, day, weekNr);
}
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
@ -443,7 +490,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
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 publishersToday = await prisma.assignment.findMany({
where: {
@ -468,11 +515,11 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
if (publishersNeeded > 0 && shift.assignments.length < goal) {
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift, 10);
if (algType == 0) {
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day);
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsCacheStatistics, day);
} else if (algType == 1) {
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsCacheStatistics, day, weekNr);
}
await AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
@ -509,10 +556,16 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
} else {
for (let i = 0; i < rankedPublishers.length; i++) {
let mainPublisher = rankedPublishers[i];
let familyMembers = availablePubsForTheShift.filter(p => (p.id !== mainPublisher.id && (p.id === mainPublisher.familyHeadId) || (p.familyHeadId === mainPublisher.id)));
let familyMembers = availablePubsForTheShift.filter(p => {
const isNotSelf = p.id !== mainPublisher.id;
const isMyFamilyHead = p.id === mainPublisher.familyHeadId;
const isMyFamilyMember = p.familyHeadId === mainPublisher.id;
return isNotSelf && (isMyFamilyHead || isMyFamilyMember);
});
if (familyMembers.length > 0 && (shift.assignments.length + familyMembers.length + 1) <= event.numberOfPublishers) {
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and " + familyMembers.length + " available family members to " + new Date(shift.startTime).getDate() + " " + shift.name);
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and family members: '" + familyMembers.map(fm => `${fm.firstName} ${fm.lastName}`).join(", ") + `' to ${new Date(shift.startTime).getDate()} ${shift.name}`);
const hasTransportInAvailability = shift.transportIn && mainPublisher.availabilities.some(avail => avail.isWithTransportIn);
const hasTransportOutAvailability = shift.transportOut && mainPublisher.availabilities.some(avail => avail.isWithTransportOut);
@ -537,7 +590,6 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
shift.assignments.push(newAssignment);
publishersToday.push(mainPublisher.id);
updateRegistry(mainPublisher.id, day, weekNr);
mainPublisher.currentMonthAssignments += 1;
for (const familyMember of familyMembers) {
const newFamilyAssignment = await prisma.assignment.create({
@ -560,7 +612,6 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
shift.assignments.push(newFamilyAssignment);
publishersToday.push(familyMember.id);
updateRegistry(familyMember.id, day, weekNr);
familyMember.currentMonthAssignments += 1;
}
break;
@ -590,7 +641,6 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
shift.assignments.push(newAssignment);
publishersToday.push(mainPublisher.id);
updateRegistry(mainPublisher.id, day, weekNr);
mainPublisher.currentMonthAssignments += 1;
break;
}
}
@ -619,7 +669,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 RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek, currentDay: Date) {
async function RankPublishersForShiftOld(publishers, stats, currentDay: Date) {
publishers.forEach(p => {
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
});
@ -672,7 +722,7 @@ async function RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek,
// 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, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
async function RankPublishersForShiftWeighted(publishers, stats, currentDay, currentWeekNr) {
// Define weights for each criterion
const weights = {
gender: 2,
@ -694,6 +744,9 @@ async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAnd
});
const calculateScoreAndPenalties = (p) => {
// apply for reaching desired shifts per month
let score = (p.isMale ? weights.gender : 0) -
(p.desiredCompletion * weights.desiredCompletion) +
((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +

View File

@ -730,10 +730,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
Генерирай смени </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 w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 2)}>
Генерирай смени 2 <span className="text-yellow-500 ml-1 text-xs"></span></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, 2)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 3 </button>
Генерирай смени 3 </button> */}
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100"
onClick={() => openConfirmModal(
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',