fix shift generation for day, ranking, family, etc...
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,7 +18,6 @@ lerna-debug.log*
|
||||
**/public/workbox-*.js.map
|
||||
**/public/worker-*.js.map
|
||||
|
||||
|
||||
.eslintcache
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@ -37,3 +36,4 @@ public/content/output/*
|
||||
public/content/output/shifts 2024.1.json
|
||||
!public/content/uploads/*
|
||||
.aider*
|
||||
/shift_generate_log_*.txt
|
||||
|
@ -143,7 +143,7 @@ function flattenRegistry(dayKey) {
|
||||
return Object.values(weekEntries).flat();
|
||||
}
|
||||
|
||||
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0) {
|
||||
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0, until) {
|
||||
|
||||
let missingPublishers = [];
|
||||
let publishersWithChangedPref = [];
|
||||
@ -176,7 +176,14 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
|
||||
if (forDay) {
|
||||
day = monthInfo.date;
|
||||
endDate.setDate(monthInfo.date.getDate() + 1);
|
||||
if (until === undefined) {
|
||||
const oneDayInMs = 24 * 60 * 60 * 1000;
|
||||
endDate = new Date(monthInfo.date.getTime() + oneDayInMs);
|
||||
}
|
||||
else {
|
||||
endDate = new Date(until);
|
||||
}
|
||||
|
||||
dayNr = monthInfo.date.getDate();
|
||||
weekNr = common.getWeekNumber(monthInfo.date);
|
||||
}
|
||||
@ -184,7 +191,7 @@ 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);
|
||||
@ -262,7 +269,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
//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);
|
||||
console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + ", for the day: " + availablePubsForTheDay.length);
|
||||
|
||||
const createdShift = await prisma.shift.create({
|
||||
data: {
|
||||
@ -294,21 +301,31 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
|
||||
}
|
||||
|
||||
day.setDate(day.getDate() + 1);
|
||||
dayNr++;
|
||||
if (common.DaysOfWeekArray[day.getDayEuropean()] === DayOfWeek.Sunday) {
|
||||
weekNr++;
|
||||
publishersThisWeek = [];
|
||||
publishers.forEach(p => p.currentWeekAssignments = 0);
|
||||
|
||||
if (forDay) { break; }
|
||||
else {
|
||||
day.setDate(day.getDate() + 1);
|
||||
dayNr++;
|
||||
if (common.DaysOfWeekArray[day.getDayEuropean()] === DayOfWeek.Sunday) {
|
||||
weekNr++;
|
||||
publishersThisWeek = [];
|
||||
publishers.forEach(p => p.currentWeekAssignments = 0);
|
||||
}
|
||||
}
|
||||
if (forDay) break;
|
||||
}
|
||||
|
||||
|
||||
let from = monthInfo.firstMonday, to = monthInfo.lastSunday;
|
||||
if (forDay) {
|
||||
from = day;
|
||||
to = endDate;
|
||||
}
|
||||
|
||||
let allShifts = await prisma.shift.findMany({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: monthInfo.firstMonday,
|
||||
lt: monthInfo.lastSunday,
|
||||
gte: from,
|
||||
lt: to,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@ -320,17 +337,23 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let publishersToday = [];
|
||||
let rankedPublishers = [];
|
||||
|
||||
// # # # # # # # # # # #
|
||||
// Second pass - prioritize shifts with transport where it is needed
|
||||
if (forDay) { }
|
||||
else {
|
||||
day = new Date(monthInfo.firstMonday);
|
||||
dayNr = 1;
|
||||
weekNr = 1;
|
||||
}
|
||||
|
||||
|
||||
console.log("\r\n\r\n\r\n" + "# ".repeat(50));
|
||||
console.log("Second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year);
|
||||
console.log("Second pass - fix transports " + day.toLocaleDateString());
|
||||
|
||||
day = new Date(monthInfo.firstMonday);
|
||||
dayNr = 1;
|
||||
weekNr = 1;
|
||||
while (day < endDate) {
|
||||
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
|
||||
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
|
||||
@ -371,7 +394,7 @@ 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', shift.startTime, true, false, false, true, false);
|
||||
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
|
||||
|
||||
let availablePublishers = availablePubsForTheShift.filter(p => {
|
||||
const hasTransportInAvailability = shift.transportIn && p.availabilities.some(avail => avail.isWithTransportIn);
|
||||
@ -386,45 +409,35 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
} else if (algType == 1) {
|
||||
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
||||
}
|
||||
// if (rankedPublishers.length > 0) {
|
||||
// const newAssignment = await prisma.assignment.create({
|
||||
// data: {
|
||||
// shift: {
|
||||
// connect: {
|
||||
// id: shift.id,
|
||||
// },
|
||||
// },
|
||||
// publisher: {
|
||||
// connect: {
|
||||
// id: rankedPublishers[0].id,
|
||||
// },
|
||||
// },
|
||||
// isWithTransport: true,
|
||||
// isConfirmed: true,
|
||||
// isBySystem: false,
|
||||
// },
|
||||
// });
|
||||
|
||||
// shift.assignments.push(newAssignment);
|
||||
// publishersToday.push(rankedPublishers[0].id);
|
||||
// updateRegistry(rankedPublishers[0].id, day, weekNr);
|
||||
// }
|
||||
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
day.setDate(day.getDate() + 1);
|
||||
if (forDay) { break; }
|
||||
else {
|
||||
day.setDate(day.getDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the rest of the shifts
|
||||
|
||||
// # # # # # # # # # # #
|
||||
// 3. Fill the rest of the shifts
|
||||
let goal = 1;
|
||||
while (goal <= 4) {
|
||||
console.log("\r\n\r\n\r\n" + "# ".repeat(50));
|
||||
console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year);
|
||||
day = new Date(monthInfo.firstMonday);
|
||||
dayNr = 1;
|
||||
weekNr = 1;
|
||||
|
||||
if (forDay) {
|
||||
}
|
||||
else {
|
||||
day = new Date(monthInfo.firstMonday);
|
||||
dayNr = 1;
|
||||
weekNr = 1;
|
||||
}
|
||||
|
||||
console.log("\r\n\r\n" + "# ".repeat(50));
|
||||
console.log("Filling shifts with " + goal + " publishers | " + day.toLocaleDateString());
|
||||
|
||||
while (day < endDate) {
|
||||
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
|
||||
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
|
||||
@ -452,7 +465,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
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 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);
|
||||
if (algType == 0) {
|
||||
@ -461,12 +474,17 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
||||
}
|
||||
|
||||
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
||||
await AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
day.setDate(day.getDate() + 1);
|
||||
if (forDay) { break; }
|
||||
else {
|
||||
day.setDate(day.getDate() + 1);
|
||||
}
|
||||
|
||||
}
|
||||
goal += 1;
|
||||
}
|
||||
@ -486,14 +504,18 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
||||
|
||||
async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheShift, rankedPublishers, publishersToday, day, weekNr) {
|
||||
if (rankedPublishers.length == 0) {
|
||||
console.log("No available publishers for shift " + shift.name);
|
||||
console.log("! ! ! No available publishers for shift " + shift.name + " ! ! !");
|
||||
} else {
|
||||
for (let i = 0; i < rankedPublishers.length; i++) {
|
||||
let mainPublisher = rankedPublishers[i];
|
||||
let familyMembers = availablePubsForTheShift.filter(p => (p.familyHeadId && p.familyHeadId === mainPublisher.familyHeadId) || (p.familyHeadId === mainPublisher.id));
|
||||
let familyMembers = availablePubsForTheShift.filter(p => (p.id !== mainPublisher.id && (p.id === mainPublisher.familyHeadId) || (p.familyHeadId === mainPublisher.id)));
|
||||
|
||||
if (familyMembers.length > 0 && (shift.assignments.length + familyMembers.length + 1) <= event.numberOfPublishers) {
|
||||
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and family members to " + new Date(shift.startTime).getDate() + " " + shift.name);
|
||||
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and " + familyMembers.length + " available family members 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);
|
||||
|
||||
const newAssignment = await prisma.assignment.create({
|
||||
data: {
|
||||
shift: {
|
||||
@ -508,7 +530,7 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
|
||||
},
|
||||
isConfirmed: false,
|
||||
isBySystem: false,
|
||||
isWithTransport: shift.requiresTransport,
|
||||
isWithTransport: (hasTransportInAvailability || hasTransportOutAvailability),
|
||||
},
|
||||
});
|
||||
shift.assignments.push(newAssignment);
|
||||
@ -540,6 +562,10 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
|
||||
break;
|
||||
} else if (familyMembers.length == 0) {
|
||||
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " 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);
|
||||
|
||||
const newAssignment = await prisma.assignment.create({
|
||||
data: {
|
||||
shift: {
|
||||
@ -554,7 +580,7 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
|
||||
},
|
||||
isConfirmed: false,
|
||||
isBySystem: false,
|
||||
isWithTransport: shift.requiresTransport,
|
||||
isWithTransport: (hasTransportInAvailability || hasTransportOutAvailability),
|
||||
},
|
||||
});
|
||||
shift.assignments.push(newAssignment);
|
||||
@ -564,17 +590,16 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) {
|
||||
async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift, maxShifts = 0) {
|
||||
//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
|
||||
|| p.currentMonthAssignments >= 6; // overwrite the desiredShiftsPerMonth to max 6 shifts per month
|
||||
return isNotAssigned && isNotAssignedToday && !isAssignedEnough;
|
||||
|| p.currentMonthAssignments >= maxShifts; // overwrite the desiredShiftsPerMonth to max 10 shifts per month
|
||||
return isNotAssigned && isNotAssignedToday && (!isAssignedEnough || maxShifts == 0);
|
||||
});
|
||||
return goodPublishers;
|
||||
}
|
||||
@ -696,10 +721,17 @@ async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAnd
|
||||
const result = calculateScoreAndPenalties(p);
|
||||
p.score = result.score;
|
||||
p.penalties = result.penalties;
|
||||
|
||||
p.finalScore = p.score;
|
||||
if (p.finalScore > 0) {
|
||||
p.penalties.forEach(penalty => {
|
||||
p.finalScore *= penalty.penalty;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort publishers based on score
|
||||
let ranked = publishers.sort((a, b) => b.score - a.score);
|
||||
let ranked = publishers.sort((a, b) => b.finalScore - a.finalScore);
|
||||
|
||||
// Log the scores and penalties of the top publisher
|
||||
if (ranked.length > 0) {
|
||||
|
@ -703,9 +703,11 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmptyDay", false, false, true)}>
|
||||
{isLoading('genEmptyDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
|
||||
създай празни ({value.getDate()}-ти) </button>
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => generateShifts("genDay", false, true, true)}>
|
||||
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => generateShifts("genDay", false, true, true, 1)}>
|
||||
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
||||
Генерирай смени ({value.getDate()}-ти) </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