added option 3, cleanup
This commit is contained in:
@ -52,13 +52,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
var action = req.query.action;
|
||||
switch (action) {
|
||||
case "generate":
|
||||
var result = await GenerateSchedule(axios,
|
||||
req.query.date?.toString() || common.getISODateOnly(new Date()),
|
||||
common.parseBool(req.query.copyFromPreviousMonth),
|
||||
common.parseBool(req.query.autoFill),
|
||||
common.parseBool(req.query.forDay),
|
||||
parseInt(req.query.type) || 0,
|
||||
);
|
||||
var date = req.query.date?.toString() || common.getISODateOnly(new Date());
|
||||
var copyFromPreviousMonth = common.parseBool(req.query.copyFromPreviousMonth);
|
||||
var autoFill = common.parseBool(req.query.autoFill);
|
||||
var forDay = common.parseBool(req.query.forDay);
|
||||
var type = 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()));
|
||||
break;
|
||||
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 (++) ###
|
||||
|
||||
@ -1236,6 +871,230 @@ async function ImportShiftsFromDocx(axios: Axios) {
|
||||
}
|
||||
|
||||
|
||||
// *********************************************************************************************************************
|
||||
//region helpers
|
||||
// *********************************************************************************************************************
|
||||
async function GenerateOptimalSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) {
|
||||
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,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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)}>
|
||||
{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)}>
|
||||
{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"
|
||||
onClick={() => openConfirmModal(
|
||||
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
|
||||
|
Reference in New Issue
Block a user