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 }) { +