fix, update and improve shift generation
This commit is contained in:
@ -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) +
|
||||
|
@ -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(
|
||||
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
|
||||
|
Reference in New Issue
Block a user