added option 3, cleanup

This commit is contained in:
Dobromir Popov
2024-06-25 19:38:55 +03:00
parent 1936a9cb78
commit 19a8963a2e
2 changed files with 241 additions and 379 deletions

View File

@ -52,13 +52,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
var action = req.query.action; var action = req.query.action;
switch (action) { switch (action) {
case "generate": case "generate":
var result = await GenerateSchedule(axios, var date = req.query.date?.toString() || common.getISODateOnly(new Date());
req.query.date?.toString() || common.getISODateOnly(new Date()), var copyFromPreviousMonth = common.parseBool(req.query.copyFromPreviousMonth);
common.parseBool(req.query.copyFromPreviousMonth), var autoFill = common.parseBool(req.query.autoFill);
common.parseBool(req.query.autoFill), var forDay = common.parseBool(req.query.forDay);
common.parseBool(req.query.forDay), var type = parseInt(req.query.type) || 0;
parseInt(req.query.type) || 0, if (type == 2) {
); var result = await GenerateOptimalSchedule(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
}
else {
var result = await GenerateSchedule(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
}
res.send(JSON.stringify(result?.error?.toString())); res.send(JSON.stringify(result?.error?.toString()));
break; break;
case "delete": case "delete":
@ -98,375 +102,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
// handle /api/data/schedule?date=2021-08-01&time=08:00:00&duration=60&service=1&provider=1
//Fix bugs in this code:
// async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMonth: boolean = false, autoFill: boolean = false, forDay: Boolean) {
// let missingPublishers: any[] = [];
// let publishersWithChangedPref: any[] = [];
// const prisma = common.getPrismaClient();
// try {
// const monthInfo = common.getMonthDatesInfo(new Date(date));
// const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1));
// //delete all shifts for this month
// if (forDay) {
// // Delete shifts only for the specific day
// await DeleteShiftsForDay(monthInfo.date);
// } else {
// // Delete all shifts for the entire month
// await DeleteShiftsForMonth(monthInfo);
// }
// console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")");
// const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":{"$eq":true}}`);
// //// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo);
// //use filterPublishers from /pages/api/data/index.ts to get publishers with stats
// let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
// let publishers = await filterPublishers("id,firstName,lastName", null, lastMonthInfo.firstMonday, true, true, false);
// //let publishersWithStatsNew = await filterPublishers("id,firstName,lastName", null, monthInfo.firstMonday, true, true, false);
// //foreach day of the month check if there is an event for this day
// //if there is an event, then generate shifts for this day based on shiftduration and event start and end time
// //####################################################GPT###########################################################
// let shiftAssignments = [];
// let day = monthInfo.firstMonday; // Start from forDay if provided, otherwise start from first Monday
// let endDate = monthInfo.lastSunday; // End at forDay + 1 day if provided, otherwise end at last Sunday
// let dayNr = 1; // Start from the day number of forDay, or 1 for the entire month
// let weekNr = 1; // Start from the week number of forDay, or 1 for the entire month
// if (forDay) {
// day = monthInfo.date;
// endDate.setDate(monthInfo.date.getDate() + 1);
// dayNr = monthInfo.date.getDate();
// weekNr = common.getWeekNumber(monthInfo.date);
// }
// let publishersThisWeek: any[] = [];
// console.log("\r\n");
// console.log("###############################################");
// console.log(" SHIFT GENERATION STARTED for " + common.getISODateOnly(monthInfo.date));
// console.log("###############################################");
// while (day < endDate) {
// const dayOfM = day.getDate();
// let dayName = common.DaysOfWeekArray[day.getDayEuropean()];
// console.log("[day " + dayNr + "] " + dayName + " " + dayOfM);
// //ToDo: rename event to cartEvent
// const event = events.find((event: { dayofweek: string }) => {
// return event.dayofweek == dayName;
// });
// if (!event) {
// console.log("no event for " + dayName);
// day.setDate(day.getDate() + 1);
// continue;
// }
// event.startTime = new Date(event.startTime);
// event.endTime = new Date(event.endTime);
// var startTime = new Date(day);
// startTime.setHours(event.startTime.getHours());
// startTime.setMinutes(event.startTime.getMinutes());
// var endTime = new Date(day);
// endTime.setHours(event.endTime.getHours());
// endTime.setMinutes(event.endTime.getMinutes());
// var shiftStart = new Date(startTime);
// var shiftEnd = new Date(startTime);
// shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
// var shiftNr = 0;
// while (shiftEnd <= endTime) {
// shiftNr++;
// const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0");
// shiftAssignments = [];
// let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime();
// console.log("[shift " + shiftNr + "] " + __shiftName + ", transport: " + (isTransportRequired ? "yes" : "no") + ", " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString() + " (end time: " + endTime.toLocaleTimeString() + ", " + event.shiftDuration + " min)");
// if (autoFill || copyFromPreviousMonth) {
// // ###########################################
// // shift cache !!!
// // ###########################################
// // get last month attendance for this shift for each week, same day of the week and same shift
// const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr);
// if (shiftLastMonthSameDay) {
// console.log("shiftCache: loaded shifts from '" + shiftLastMonthSameDay.startTime + "' for: " + day);
// //log shiftLastMonthSameDay.assignments.publisher names
// console.log("last month attendance for shift " + shiftNr + " (" + __shiftName + ") : " + shiftLastMonthSameDay.assignments.map((a: { publisher: { firstName: string; lastName: string; }; }) => a.publisher.firstName + " " + a.publisher.lastName).join(", "));
// for (var i = 0; i < shiftLastMonthSameDay.assignments.length; i++) {
// let sameP = shiftLastMonthSameDay.assignments[i].publisher;
// let name = sameP.firstName + " " + sameP.lastName;
// console.log("shiftCache: considerig publisher: " + sameP.firstName + " " + sameP.lastName + ". Checking if he is available for this shift...");
// //get availability for the same dayofweek and time (< startTime, > endTime) OR exact date (< startTime, > endTime)
// // Query for exact date match
// let availability = (await prisma.availability.findMany({
// where: {
// publisherId: sameP.id,
// dayOfMonth: dayOfM,
// startTime: {
// lte: shiftStart,
// },
// endTime: {
// gte: shiftEnd,
// },
// },
// }))[0] || null;
// if (copyFromPreviousMonth) {
// //copy from previous month without checking availability
// console.log("shiftCache: copy from previous month. Аvailability is " + (availability ? "available" : "not available")
// + ". Adding him to the new scedule as " + (availability ? "confirmed" : "tentative") + ".");
// shiftAssignments.push({ publisherId: sameP.id, isConfirmed: availability ? false : true });
// } else {
// // check if the person filled the form this month
// const allAvailabilities = await prisma.availability.findMany({
// where: {
// publisherId: sameP.id,
// isFromPreviousAssignment: false,
// },
// });
// // // ?? get the date on the same weeknr and dayofweek last month, and check if there is an availability for the same day of the week and required time
// // if (!availability) {
// // // check if there is an availability for the same day of the week and required time
// // availability = allAvailabilities.filter((a: { dayofweek: any; startTime: Date; endTime: Date; }) => {
// // return a.dayofweek === event.dayofweek && a.startTime <= startTime && a.endTime >= endTime;
// // })[0] || null;
// // }
// // var availability = allAvailabilities.find((a) => {
// // return (a.dayofweek === event.dayofweek && a.dayOfMonth == null) || a.dayOfMonth == dayOfM;
// // });
// //publishers not filled the form will not have an email with @, but rather as 'firstname.lastname'.
// //We will add them to the schedule as manual override until they fill the form
// //ToDo this logic is not valid in all cases.
// if (!availability && sameP.email.includes("@")) {
// if (!publishersWithChangedPref.includes(name)) {
// //publishersWithChangedPref.push(name);
// }
// console.log("shiftCache: publisher is not available for this shift. Available days: " + allAvailabilities.filter((a: { dayOfMonth: any; }) => a.dayOfMonth === dayOfM).map((a) => a.dayofweek + " " + a.dayOfMonth).join(", "));
// //continue;
// }
// if (availability) {
// console.log("shiftCache: publisher is available for this shift. Available days: " + availability.dayofweek + " " + availability.dayOfMonth + " " + availability.startTime + " - " + availability.endTime);
// console.log("shiftCache: publisher is available for this shift OR manual override is set. Adding him to the new scedule.");
// shiftAssignments.push({ publisherId: sameP.id });
// }
// else {
// // skip publishers without availability now
// // console.warn("NO publisher availability found! for previous assignment for " + name + ". Assuming he does not have changes in his availability. !!! ADD !!! him to the new scedule but mark him as missing.");
// // if (!missingPublishers.includes(name)) {
// // missingPublishers.push(name);
// // }
// // try {
// // console.log("shiftCache: publisher was last month assigned to this shift but he is not in the system. Adding him to the system with id: " + sameP.id);
// // shiftAssignments.push({ publisherId: sameP.id, });
// // } catch (e) {
// // console.error(`shiftCache: error adding MANUAL publisher to the system(${sameP.email} ${sameP.firstName} ${sameP.lastName}): ` + e);
// // }
// }
// }
// }
// // ###########################################
// // shift CACHE END
// // ###########################################
// console.log("searching available publisher for " + dayName + " " + __shiftName);
// if (!copyFromPreviousMonth) {
// /* We chave the following data:
// availabilities:(6) [{…}, {…}, {…}, {…}, {…}, {…}]
// currentDayAssignments:0
// currentMonthAssignments:2
// currentMonthAvailability:(2) [{…}, {…}]
// currentMonthAvailabilityDaysCount:2
// currentMonthAvailabilityHoursCount:3
// currentWeekAssignments:0
// firstName:'Алесия'
// id:'clqjtcrqj0008oio8kan5lkjn'
// lastName:'Сейз'
// previousMonthAssignments:2
// */
// // until we reach event.numberOfPublishers, we will try to fill the shift with publishers from allAvailablePublishers with the following priority:
// // do multiple passes, reecalculating availabilityIndex for each publisher after each pass.
// // !!! Never assign the same publisher twice to the same day! (currentDayAssignments > 0)
// // PASS 1: Prioritize publishers with little currentMonthAvailabilityHoursCount ( < 5 ), as they may not have another opportunity to serve this month
// // PASS 2: try to fill normally based on availabilityIndex, excluding those who were assigned this week
// // PASS 3: try to fill normally based on availabilityIndex, including those who were assigned this week and weighting the desiredShiftsPerMonth
// // PASS 4: include those without availability this month - based on old availabilities and assignments for this day of the week.
// // push found publisers to shiftAssignments with: .push({ publisherId: publisher.id }); and update publisher stats in new function: addAssignmentToPublisher(shiftAssignments, publisher)
// // ---------------------------------- new code ---------------------------------- //
// // get all publishers who are available for this SPECIFIC day and WEEKDAY
// const queryParams = new URLSearchParams({
// action: 'filterPublishers',
// assignments: 'true',
// availabilities: 'true',
// date: common.getISODateOnly(shiftStart),
// select: 'id,firstName,lastName,isActive,desiredShiftsPerMonth'
// });
// let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data;
// let availablePublishers = allAvailablePublishers;
// let publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// // LEVEL 1: Prioritize publishers with little currentMonthAvailabilityHoursCount ( < 5 ), as they may not have another opportunity to serve this month
// // get publishers with little currentMonthAvailabilityHoursCount ( < 5 )
// // let availablePublishers = allAvailablePublishers.filter((p: { currentMonthAvailabilityHoursCount: number; }) => p.currentMonthAvailabilityHoursCount < 5);
// // // log all available publishers with their currentMonthAvailabilityHoursCount
// // console.info("PASS 1: availablePublishers for this shift with currentMonthAvailabilityHoursCount < 5: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
// // availablePublishers.slice(0, publishersNeeded).forEach((p: { id: any; }) => { addAssignmentToPublisher(shiftAssignments, p); });
// // publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// // LEVEL 2+3: try to fill normally based on availabilityIndex, excluding those who were assigned this week
// // get candidates that are not assigned this week, and which have not been assigned this month as mutch as the last month.
// // calculate availabilityIndex for each publisher based on various factors:
// // 1. currentMonthAssignments - lastMonth (weight 50%)
// // 2. desiredShiftsPerMonth (weight 30%)
// // 3. publisher type (weight 20%) - regular, auxiliary, pioneer, special, bethel, etc.. (see publisherType in publisher model). exclude betelites who were assigned this month. (index =)
// //calculate availabilityIndex:
// allAvailablePublishers.forEach((p: { currentMonthAssignments: number; desiredShiftsPerMonth: number; publisherType: string; }) => {
// // 1. currentMonthAssignments - lastMonth (weight 50%)
// // 2. desiredShiftsPerMonth (weight 30%)
// // 3. publisher type (weight 20%) - regular, auxiliary, pioneer, special, bethel, etc.. (see publisherType in publisher model). exclude betelites who were assigned this month. (index =)
// p.availabilityIndex = Math.round(((p.currentMonthAssignments - p.previousMonthAssignments) * 0.5 + p.desiredShiftsPerMonth * 0.3 + (p.publisherType === "bethelite" ? 0 : 1) * 0.2) * 100) / 100;
// });
// // use the availabilityIndex to sort the publishers
// // LEVEL 2: remove those who are already assigned this week (currentWeekAssignments > 0), order by !availabilityIndex
// availablePublishers = allAvailablePublishers.filter((p: { currentWeekAssignments: number; }) => p.currentWeekAssignments === 0)
// .sort((a: { availabilityIndex: number; }, b: { availabilityIndex: number; }) => a.availabilityIndex - b.availabilityIndex);
// console.warn("PASS 2: availablePublishers for this shift after removing already assigned this week: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
// availablePublishers.slice(0, publishersNeeded).forEach((p: { id: any; }) => { addAssignmentToPublisher(shiftAssignments, p); });
// publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// // LEVEL 3: order by !availabilityIndex
// availablePublishers = allAvailablePublishers.sort((a: { availabilityIndex: number; }, b: { availabilityIndex: number; }) => a.availabilityIndex - b.availabilityIndex);
// console.warn("PASS 3: availablePublishers for this shift including already assigned this week: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
// availablePublishers.slice(0, publishersNeeded).forEach((p: { id: any; }) => { addAssignmentToPublisher(shiftAssignments, p); });
// publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// // LEVEL 4: include those without availability this month - based on old availabilities and assignments for this day of the week.
// // get candidates that are not assigned this week, and which have not been assigned this month as mutch as the last month.
// //query the api again for all publishers with assignments and availabilities for this day of the week including from old assignments (set filterPublishers to false)
// availablePublishers = await filterPublishers("id,firstName,lastName", null, shiftStart, false, true, true);
// console.warn("PASS 4: availablePublishers for this shift including weekly and old assignments: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
// function oldCode() {
// // ---------------------------------- old code ---------------------------------- //
// // console.warn("allAvailablePublishers: " + allAvailablePublishers.length);
// // // remove those who are already assigned this week (currentWeekAssignments > 0)//, # OLD: order by !availabilityIndex
// // let availablePublishers = allAvailablePublishers.filter((p: { currentWeekAssignments: number; }) => p.currentWeekAssignments === 0);
// // console.warn("availablePublishers for this shift after removing already assigned this week: " + availablePublishers.length + " (" + (event.numberOfPublishers - shiftAssignments.length) + " needed)");
// // if (availablePublishers.length === 0) {
// // console.error(`------------------- no available publishers for ${dayName} ${dayOfM}!!! -------------------`);
// // // Skipping the rest of the code execution
// // //return;
// // }
// // let msg = `FOUND ${availablePublishers.length} publishers for ${dayName} ${dayOfM}, ${__shiftName} . ${event.numberOfPublishers - shiftAssignments.length} needed\r\n: `;
// // msg += availablePublishers.map((p: { firstName: any; lastName: any; asignmentsThisMonth: any; availabilityIndex: any; }) => `${p.firstName} ${p.lastName} (${p.asignmentsThisMonth}:${p.availabilityIndex})`).join(", ");
// // console.log(msg);
// // // ---------------------------------- old code ---------------------------------- //
// } // end of old code
// }
// }
// }
// //###############################################################################################################
// // create shift assignmens
// //###############################################################################################################
// // using prisma client:
// // https://stackoverflow.com/questions/65950407/prisma-many-to-many-relations-create-and-connect
// // connect publishers to shift
// const createdShift = await prisma.shift.create({
// data: {
// startTime: shiftStart,
// endTime: shiftEnd,
// name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(),
// requiresTransport: isTransportRequired,
// cartEvent: {
// connect: {
// id: event.id,
// },
// },
// assignments: {
// create: shiftAssignments.map((a) => {
// return { publisher: { connect: { id: a.publisherId } }, isConfirmed: a.isConfirmed };
// }),
// },
// },
// });
// shiftStart = new Date(shiftEnd);
// shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
// }
// day.setDate(day.getDate() + 1);
// dayNr++;
// let weekDay = common.DaysOfWeekArray[day.getDayEuropean()]
// if (weekDay == DayOfWeek.Sunday) {
// weekNr++;
// publishersThisWeek = [];
// publishers.forEach((p: { currentWeekAssignments: number; }) => {
// p.currentWeekAssignments = 0;
// });
// }
// //the whole day is done, go to next day. break if we are generating for a specific day
// if (forDay) {
// break;
// }
// }
// //###################################################GPT############################################################
// if (!forDay) {
// const fs = require("fs");
// //fs.writeFileSync("./content/publisherShiftStats.json", JSON.stringify(publishers, null, 2));
// //fs.writeFileSync("./content/publishersWithChangedPref.json", JSON.stringify(publishersWithChangedPref, null, 2));
// //fs.writeFileSync("./content/missingPublishers.json", JSON.stringify(missingPublishers, null, 2));
// console.log("###############################################");
// console.log(" DONE CREATING SCHEDULE FOR " + monthInfo.monthName + " " + monthInfo.year);
// console.log("###############################################");
// }
// //create shifts using API
// // const { data: createdShifts } = await axios.post(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/api/data/shifts`, shiftsToCreate);
// //const { data: allshifts } = await axios.get(`/api/data/shifts`);
// return {}; //allshifts;
// }
// catch (error) {
// console.log(error);
// return { error: error };
// }
// }
// async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMonth: boolean = false, autoFill: boolean = false, forDay: Boolean) {
// await GenerateSchedule(axios, date, true, autoFill, forDay);
// }
// ### COPIED TO shift api (++) ### // ### COPIED TO shift api (++) ###
@ -1236,6 +871,230 @@ async function ImportShiftsFromDocx(axios: Axios) {
} }
// ********************************************************************************************************************* async function GenerateOptimalSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) {
//region helpers const prisma = common.getPrismaClient();
// ********************************************************************************************************************* try {
const monthInfo = common.getMonthDatesInfo(new Date(date));
const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1));
if (forDay) {
await DeleteShiftsForDay(monthInfo.date);
} else {
await DeleteShiftsForMonth(monthInfo);
}
const events = await prisma.cartEvent.findMany({
where: {
isActive: true
}
});
let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
let publishers = await data.getAllPublishersWithStatisticsMonth(date, false, false);
let day = new Date(monthInfo.firstMonday);
let endDate = monthInfo.lastSunday;
let weekNr = 1;
if (forDay) {
day = monthInfo.date;
endDate = new Date(monthInfo.date.getTime() + 86400000); // +1 day
weekNr = common.getWeekNumber(monthInfo.date);
}
let allShifts = [];
// First pass: Generate shifts and copy assignments from the previous month
while (day < endDate) {
let dayShifts = await generateShiftsForDay(day, events, shiftsLastMonth, weekNr, copyFromPreviousMonth);
allShifts = [...allShifts, ...dayShifts];
day.setDate(day.getDate() + 1);
if (common.DaysOfWeekArray[day.getDayEuropean()] === DayOfWeek.Sunday) {
weekNr++;
}
if (forDay) break;
}
// Second pass: Optimize assignments
allShifts = await optimizeAssignments(allShifts, publishers, events);
// Save optimized shifts to the database
for (let shift of allShifts) {
await saveShiftToDB(shift);
}
return {};
} catch (error) {
console.log(error);
return { error: error };
}
}
async function generateShiftsForDay(day, events, shiftsLastMonth, weekNr, copyFromPreviousMonth) {
const prisma = common.getPrismaClient();
let dayShifts = [];
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
let dayName = common.DaysOfWeekArray[day.getDayEuropean()];
const event = events.find((event) => event.dayofweek == dayName && (event.dayOfMonth == null || event.dayOfMonth == day.getDate()));
if (!event) return dayShifts;
let startTime = new Date(day);
startTime.setHours(event.startTime.getHours(), event.startTime.getMinutes());
let endTime = new Date(day);
endTime.setHours(event.endTime.getHours(), event.endTime.getMinutes());
let shiftStart = new Date(startTime);
let shiftEnd = new Date(startTime);
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
let shiftNr = 0;
while (shiftEnd <= endTime) {
shiftNr++;
let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime();
let shift = {
startTime: new Date(shiftStart),
endTime: new Date(shiftEnd),
name: `${event.dayofweek} ${shiftStart.toLocaleTimeString()} - ${shiftEnd.toLocaleTimeString()}`,
requiresTransport: isTransportRequired,
cartEventId: event.id,
assignments: [],
};
if (copyFromPreviousMonth) {
const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr);
if (shiftLastMonthSameDay) {
shift.assignments = shiftLastMonthSameDay.assignments
.map(a => ({
publisherId: a.publisher.id,
isConfirmed: true,
isWithTransport: a.isWithTransport
}));
}
}
dayShifts.push(shift);
shiftStart = new Date(shiftEnd);
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
}
return dayShifts;
}
async function optimizeAssignments(allShifts, publishers, events) {
let scheduledPubsPerDayAndWeek = {};
for (let shift of allShifts) {
const event = events.find(e => e.id === shift.cartEventId);
const day = new Date(shift.startTime);
const weekNr = common.getWeekNumber(day);
let availablePubs = await getAvailablePublishersForShiftNew(shift.startTime, shift.endTime, publishers, []);
availablePubs = filterPublishersForShift(availablePubs, shift, scheduledPubsPerDayAndWeek, day, weekNr);
while (shift.assignments.length < event.numberOfPublishers && availablePubs.length > 0) {
const rankedPubs = rankPublishersForShift(availablePubs, scheduledPubsPerDayAndWeek, day, weekNr);
const selectedPub = rankedPubs[0];
shift.assignments.push({
publisherId: selectedPub.id,
isConfirmed: true,
isWithTransport: shift.requiresTransport && (selectedPub.isWithTransportIn || selectedPub.isWithTransportOut)
});
updateRegistry(selectedPub.id, day, weekNr, scheduledPubsPerDayAndWeek);
selectedPub.currentMonthAssignments++;
availablePubs = availablePubs.filter(p => p.id !== selectedPub.id);
}
}
return allShifts;
}
function filterPublishersForShift(publishers, shift, scheduledPubsPerDayAndWeek, day, weekNr) {
const dayKey = common.getISODateOnly(day);
return publishers.filter(p => {
const isNotAssigned = !shift.assignments.some(a => a.publisherId === p.id);
const isNotAssignedToday = !flattenRegistry(scheduledPubsPerDayAndWeek[dayKey]).includes(p.id);
const isNotOverAssigned = p.currentMonthAssignments < p.desiredShiftsPerMonth;
const isNotAssignedThisWeek = !Object.keys(scheduledPubsPerDayAndWeek)
.filter(key => common.getWeekNumber(new Date(key)) === weekNr)
.some(key => flattenRegistry(scheduledPubsPerDayAndWeek[key]).includes(p.id));
return isNotAssigned && isNotAssignedToday && isNotOverAssigned && isNotAssignedThisWeek;
});
}
function rankPublishersForShift(publishers, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
const weights = {
gender: 2,
desiredCompletion: 3,
availability: 2,
lastMonthCompletion: 3,
currentAssignments: 1
};
const totalWeight = Object.values(weights).reduce((acc, val) => acc + val, 0);
Object.keys(weights).forEach(key => {
weights[key] /= totalWeight;
});
publishers.forEach(p => {
p.score = calculatePublisherScore(p, weights, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr);
});
return publishers.sort((a, b) => b.score - a.score);
}
function calculatePublisherScore(publisher, weights, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
let score = (publisher.isMale ? weights.gender : 0) -
((publisher.currentMonthAssignments / publisher.desiredShiftsPerMonth) * weights.desiredCompletion) +
((1 - publisher.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
((publisher.previousMonthAssignments / publisher.currentMonthAssignments) * weights.lastMonthCompletion) -
(publisher.currentMonthAssignments * weights.currentAssignments);
// Apply penalties for nearby assignments
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];
if (flattenRegistry(scheduledPubsPerDayAndWeek[previousDayKey]).includes(publisher.id) ||
flattenRegistry(scheduledPubsPerDayAndWeek[nextDayKey]).includes(publisher.id)) {
score *= penalty;
}
}
return score;
}
async function saveShiftToDB(shift) {
const prisma = common.getPrismaClient();
await prisma.shift.create({
data: {
startTime: shift.startTime,
endTime: shift.endTime,
name: shift.name,
requiresTransport: shift.requiresTransport,
cartEvent: {
connect: {
id: shift.cartEventId,
},
},
assignments: {
create: shift.assignments.map(a => ({
publisher: {
connect: { id: a.publisherId }
},
isWithTransport: a.isWithTransport,
isConfirmed: a.isConfirmed,
isBySystem: true,
})),
},
},
});
}

View File

@ -723,6 +723,9 @@ 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("genDay", true, true, null, 1)}> <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>)} {isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 2 </button> Генерирай смени 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)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 3 </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(
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?', 'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',