diff --git a/pages/api/shiftgenerate.ts b/pages/api/shiftgenerate.ts
index 4a65c6f..625b9d3 100644
--- a/pages/api/shiftgenerate.ts
+++ b/pages/api/shiftgenerate.ts
@@ -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,
+ })),
+ },
+ },
+ });
+}
+
diff --git a/pages/cart/calendar/index.tsx b/pages/cart/calendar/index.tsx
index 6148d6e..fd55e48 100644
--- a/pages/cart/calendar/index.tsx
+++ b/pages/cart/calendar/index.tsx
@@ -723,6 +723,9 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
+