From cb129a37094310cff2a2837705043e81d04b8e33 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Thu, 23 May 2024 02:53:15 +0300 Subject: [PATCH] brand new schedule auto generation --- pages/api/shiftgenerate.ts | 1207 ++++++++++++++++++++++++------------ src/helpers/common.js | 21 +- src/helpers/data.js | 498 +++++++++++++-- 3 files changed, 1284 insertions(+), 442 deletions(-) diff --git a/pages/api/shiftgenerate.ts b/pages/api/shiftgenerate.ts index f0137f6..46eb821 100644 --- a/pages/api/shiftgenerate.ts +++ b/pages/api/shiftgenerate.ts @@ -11,6 +11,7 @@ import { filterPublishers, /* other functions */ } from './index'; import CAL from "../../src/helpers/calendar"; //const common = require("@common"); import common from "../../src/helpers/common"; +import data from "../../src/helpers/data"; import { Axios } from 'axios'; export default handler; @@ -54,7 +55,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { common.parseBool(req.query.copyFromPreviousMonth), common.parseBool(req.query.autoFill), common.parseBool(req.query.forDay)); - res.send(JSON.stringify(result.error?.toString())); + res.send(JSON.stringify(result?.error?.toString())); break; case "delete": result = await DeleteSchedule(axios, req.query.date, common.parseBool(req.query.forDay)); @@ -92,44 +93,403 @@ 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[] = []; +// 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 (++) ### +async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) { + let missingPublishers = []; + let publishersWithChangedPref = []; 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 + const events = await prisma.cartEvent.findMany({ + where: { + isActive: true + } + }); 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 publishers = await data.getAllPublishersWithStatisticsMonth(date, false, false); 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 - + let day = new Date(monthInfo.firstMonday); + let endDate = monthInfo.lastSunday; + let dayNr = 1; + let weekNr = 1; if (forDay) { day = monthInfo.date; @@ -138,23 +498,18 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont 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("###############################################"); + let publishersThisWeek = []; + // 0. generate shifts and assign publishers from the previous month if still available while (day < endDate) { + let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false); + console.log("passing schedule generation for " + day.toLocaleDateString()); const dayOfM = day.getDate(); + let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day); 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; - }); + const event = events.find((event) => event.dayofweek == dayName && (event.dayOfMonth == null || event.dayOfMonth == dayOfM)); + if (!event) { - console.log("no event for " + dayName); day.setDate(day.getDate() + 1); continue; } @@ -162,235 +517,62 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont event.startTime = new Date(event.startTime); event.endTime = new Date(event.endTime); - var startTime = new Date(day); + let startTime = new Date(day); startTime.setHours(event.startTime.getHours()); startTime.setMinutes(event.startTime.getMinutes()); - var endTime = new Date(day); + let endTime = new Date(day); endTime.setHours(event.endTime.getHours()); endTime.setMinutes(event.endTime.getMinutes()); - var shiftStart = new Date(startTime); - var shiftEnd = new Date(startTime); + let shiftStart = new Date(startTime); + let shiftEnd = new Date(startTime); shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration); - var shiftNr = 0; + let 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 !!! - // ########################################### + const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr); - // get last month attendance for this shift for each week, same day of the week and same shift - const shiftLastMonthSameDay = getShiftFromLastMonth(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(", ")); + if (shiftLastMonthSameDay) { + for (let assignment of shiftLastMonthSameDay.assignments) { + let publisher = assignment.publisher; + console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName); + let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr); + console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime)); - 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' + if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) { + shiftAssignments.push({ + publisherId: publisher.id, + isConfirmed: true, + isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut }); - 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 + publishersThisWeek.push(publisher.id); } } } - //############################################################################################################### - // create shift assignmens - //############################################################################################################### - // using prisma client: - // https://stackoverflow.com/questions/65950407/prisma-many-to-many-relations-create-and-connect - // connect publishers to shift + + + let publishersNeeded = event.numberOfPublishers - shiftAssignments.length; + //ToDo: check if getAvailablePublishersForShift is working correctly. It seems not to! + let availablePublishers = await getAvailablePublishersForShiftNew(shiftStart, shiftEnd, availablePubsForTheDay, publishersThisWeek); + + console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length); + + // Prioritize publishers with minimal availability + // SKIP ADDING PUBLISHERS FOR NOW + // availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); + + // for (let i = 0; i < publishersNeeded; i++) { + // if (availablePublishers[i]) { + // shiftAssignments.push({ publisherId: availablePublishers[i].id }); + // publishersThisWeek.push(availablePublishers[i].id); + // } + // } + const createdShift = await prisma.shift.create({ data: { startTime: shiftStart, @@ -404,68 +586,318 @@ async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMont }, assignments: { create: shiftAssignments.map((a) => { - return { publisher: { connect: { id: a.publisherId } }, isConfirmed: a.isConfirmed }; + return { + publisher: { + connect: { id: a.publisherId } + }, + isWithTransport: a.isWithTransport, + isConfirmed: a.isConfirmed, + isBySystem: true, + }; }), }, }, }); - 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) { + if (common.DaysOfWeekArray[day.getDayEuropean()] === 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; + publishers.forEach(p => p.currentWeekAssignments = 0); } + if (forDay) break; } - //###################################################GPT############################################################ + + let allShifts = await prisma.shift.findMany({ + where: { + startTime: { + gte: monthInfo.firstMonday, + lt: monthInfo.lastSunday, + }, + }, + include: { + assignments: { + include: { + publisher: true, + }, + }, + }, + }); + + let publishersToday = []; + + // 2. First pass - prioritize shifts with transport where it is needed + console.log(" second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year); + day = new Date(monthInfo.firstMonday); + dayNr = 1; + weekNr = 1; + while (day < endDate) { + let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day); + let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]); + if (event) { + let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false); + let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day)); + + //let publishersToday = shifts.flatMap(s => s.assignments.map(a => a.publisher.id)); + //get all publishers assigned for the day from the database + let publishersToday = await prisma.assignment.findMany({ + where: { + shift: { + startTime: { + gte: common.getStartOfDay(day), + lt: common.getEndOfDay(day), + }, + }, + }, + select: { + publisherId: true, + }, + }).then((assignments) => assignments.map(a => a.publisherId)); + + + let transportShifts = shifts.filter(s => s.requiresTransport); + transportShifts[0].transportIn = true; + if (transportShifts.length > 1) { + transportShifts[1].transportOut = true; + } + // if there are no transport yet: + // transportShifts.forEach(async shift => { + for (const shift of transportShifts) { + //todo: replace that with transport check + let publishersNeeded = event.numberOfPublishers - shift.assignments.length; + let transportCapable = shift.assignments.filter(a => a.isWithTransport); + let tramsportCapableMen = transportCapable.filter(a => a.publisher.isMale); + let mayNeedTransport = transportCapable.length < 2 && tramsportCapableMen.length < 1; + + if (!mayNeedTransport) { + console.log("shift " + shift.name + " has transport (" + transportCapable.length + " transport capable)"); + } + else if (publishersNeeded > 0) { + console.log("shift " + shift.name + " requires transport (" + transportCapable.length + " transport capable)"); + // let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id)); + let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', shift.startTime, true, false, false, true, false); + // let availablePublishers = availablePubsForTheShift.filter(p => + // (shift.transportIn && p.availabilities.some(avail => (avail.isWithTransportIn)) + // || (shift.transportOut && p.availabilities.some(avail => (avail.isWithTransportOut)))) + // && !shift.assignments.some(a => a.publisher.id === p.id)); + let availablePublishers = availablePubsForTheShift.filter(p => { + const hasTransportInAvailability = shift.transportIn && p.availabilities.some(avail => avail.isWithTransportIn); + const hasTransportOutAvailability = shift.transportOut && p.availabilities.some(avail => avail.isWithTransportOut); + const isNotAssigned = !shift.assignments.some(a => a.publisher.id === p.id); + const isNotAssignedToday = !publishersToday.includes(p.id) + + return (hasTransportInAvailability || hasTransportOutAvailability) && isNotAssigned && isNotAssignedToday; + }); + // rank publishers based on different factors + let rankedPublishers = await RankPublishersForShift(availablePublishers) + if (rankedPublishers.length > 0) { + const newAssignment = await prisma.assignment.create({ + data: { + shift: { + connect: { + id: shift.id, + }, + }, + publisher: { + connect: { + id: rankedPublishers[0].id, + }, + }, + isWithTransport: true, + isConfirmed: true, + isBySystem: false, + }, + }); + + shift.assignments.push(newAssignment); + publishersToday.push(rankedPublishers[0].id); + } + + } + } + } + + day.setDate(day.getDate() + 1); + } + + + + // 3. next passes - fill the rest of the shifts + let goal = 1; // 4 to temporary skip + while (goal <= 4) { + console.log("#".repeat(50)); + console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year); + day = new Date(monthInfo.firstMonday); + dayNr = 1; + weekNr = 1; + while (day < endDate) { + let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day); + let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]); + if (event) { + let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false); + let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day)); + // let publishersToday = shifts.flatMap(s => s.assignments.map(a => a.publisher?.id)); + let publishersToday = await prisma.assignment.findMany({ + where: { + shift: { + startTime: { + gte: common.getStartOfDay(day), + lt: common.getEndOfDay(day), + }, + }, + }, + select: { + publisherId: true, + }, + }).then((assignments) => assignments.map(a => a.publisherId)); + + let shiftsToFill = shifts.filter(s => s.assignments.length < goal); + console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers"); + + for (const shift of shiftsToFill) { + let publishersNeeded = event.numberOfPublishers - shift.assignments.length; + if (publishersNeeded > 0 && shift.assignments.length < goal) { + let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', shift.startTime, true, false, false, true, false); + + //ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later. + let availablePublishers = availablePubsForTheShift.filter(p => { + + const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id); + const isNotAssignedToday = !publishersToday.includes(p.id); + const isAssignedEnough = p.currentMonthAssignments >= p.desiredShiftsPerMonth; + //if (isAssignedEnough) console.log(p.firstName + " " + p.lastName + " is assigned enough: " + p.currentMonthAssignments + " >= " + p.desiredShiftsPerMonth); + + return isNotAssigned && isNotAssignedToday && !isAssignedEnough; + + + }); + + shift.availablePublishers = availablePublishers.length; + let rankedPublishers = await RankPublishersForShift(availablePublishers) + if (rankedPublishers.length == 0) { + console.log("No available publishers for shift " + shift.name); + } else if (rankedPublishers.length > 0) { + console.log("Assigning " + rankedPublishers[0].firstName + " " + rankedPublishers[0].lastName + " to " + new Date(shift.startTime).getDate() + " " + shift.name); + const newAssignment = await prisma.assignment.create({ + data: { + shift: { + connect: { + id: shift.id, + }, + }, + publisher: { + connect: { + id: rankedPublishers[0].id, + }, + }, + isConfirmed: true, + isBySystem: false, + }, + }); + shift.assignments.push(newAssignment); + publishersToday.push(rankedPublishers[0].id); + } + //check if publisher.familyMembers are also available and add them to the shift. ToDo: test case + let familyMembers = availablePubsForTheShift.filter(p => p.familyHeadId && p.familyHeadId === rankedPublishers[0].familyHeadId); + if (familyMembers.length > 0) { + familyMembers.forEach(async familyMember => { + if (shift.assignments.length < event.numberOfPublishers) { + console.log("Assigning " + familyMember.firstName + " " + familyMember.lastName + " to " + shift.startDate.getDate() + " " + shift.name); + const newAssignment = await prisma.assignment.create({ + data: { + shift: { + connect: { + id: shift.id, + }, + }, + publisher: { + connect: { + id: familyMember.id, + }, + }, + isConfirmed: true, + isBySystem: false, + }, + }); + shift.assignments.push(newAssignment); + publishersToday.push(familyMember.id); + } + }); + + } + + } + }; + } + + day.setDate(day.getDate() + 1); + } + goal += 1 + } + 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) { + return {}; + } catch (error) { console.log(error); return { error: error }; } } -function addAssignmentToPublisher(shiftAssignments: any[], publisher: Publisher) { - shiftAssignments.push({ publisherId: publisher.id }); - publisher.currentWeekAssignments++ || 1; - publisher.currentDayAssignments++ || 1; - publisher.currentMonthAssignments++ || 1; - //console.log(`manual assignment: ${dayName} ${dayOfM} ${shiftStart}:${shiftEnd} ${p.firstName} ${p.lastName} ${p.availabilityIndex} ${p.currentMonthAssignments}`); - console.log(`manual assignment: ${publisher.firstName} ${publisher.lastName} ${publisher.currentMonthAssignments}`); - return publisher; -} +//General guidelines affecting ranking of publishers for shift assignment +// 0. generate shifts and assign publishers from the previous month if still available +// 1. Make sure we always put people only when they are available. +// 2. First provision one male or two females that are available for transport in the first and last shifts. +// 3, Then gradually fill all other shifts with day by day troughout the whole month (monthInfo.firstMonday to .lastSunday) with first one, then two, then 3 and wherever possible more (up to CartEvent.numberOfPublishers number) +// 4. Some publishers are available only at specific time (somoetimes only once) and other are more available. if people are available only for this time, prioritize them so they are not left behind. +// 5. prioritize based on publisher's desiredShiftsPerMonth and previous months assignments. +// 6. Idealy noone should be more than once a week. disqualify publishers already on a shift this week. only assign them if there are no other options and we have less than 3 publishers on a specific shift. -async function DeleteShiftsForMonth(monthInfo: any) { +//sort publishers to rank the best option for the current shift assignment +async function RankPublishersForShift(publishers) { + publishers.forEach(p => { + p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments; + }); + + let ranked = publishers.sort((a, b) => { + // males first (descending) + if (a.isMale && !b.isMale) return -1; + + // desired completion (normalized 0%=0 - 100%=1) ; lower first + const desiredCompletion = a.currentMonthAssignments / a.desiredShiftsPerMonth - b.currentMonthAssignments / b.desiredShiftsPerMonth; + //console.log(a.firstName + " " + a.lastName + " desiredCompletion: " + desiredCompletion, a.currentMonthAssignments, "/", a.desiredShiftsPerMonth); + if (desiredCompletion !== 0) return desiredCompletion; + // const desiredDifference = b.DesiredMinusCurrent - a.DesiredMinusCurrent; + // if (desiredDifference !== 0) return desiredDifference; + const desiredCompletionA = a.currentMonthAssignments / a.desiredShiftsPerMonth; + const desiredCompletionB = b.currentMonthAssignments / b.desiredShiftsPerMonth; + console.log(`${a.firstName} ${a.lastName} desiredCompletion: ${desiredCompletionA}, ${a.currentMonthAssignments} / ${a.desiredShiftsPerMonth}`); + // console.log(`${b.firstName} ${b.lastName} desiredCompletion: ${desiredCompletionB}, ${b.currentMonthAssignments} / ${b.desiredShiftsPerMonth}`); + + // less available first (ascending) + const availabilityDifference = a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount; + if (availabilityDifference !== 0) return availabilityDifference; + + // biggest difference between desired and current assignments first (descending) + const desiredCurrentDifference = b.DesiredMinusCurrent - a.DesiredMinusCurrent; + if (desiredCurrentDifference !== 0) return desiredCurrentDifference; + + // less assigned first (ascending) + return a.currentMonthAssignments - b.currentMonthAssignments; + }); + + return ranked; +} +async function DeleteShiftsForMonth(monthInfo) { try { const prisma = common.getPrismaClient(); await prisma.shift.deleteMany({ @@ -481,7 +913,7 @@ async function DeleteShiftsForMonth(monthInfo: any) { } } -async function DeleteShiftsForDay(date: Date) { +async function DeleteShiftsForDay(date) { const prisma = common.getPrismaClient(); try { // Assuming shifts do not span multiple days, so equality comparison is used @@ -498,7 +930,6 @@ async function DeleteShiftsForDay(date: Date) { } } - async function getShiftsFromLastMonth(monthInfo) { const prisma = common.getPrismaClient(); // Fetch shifts for the month @@ -527,7 +958,7 @@ async function getShiftsFromLastMonth(monthInfo) { })); } -function getShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) { +function findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) { let weekDay = common.DaysOfWeekArray[day.getDayEuropean()]; return shiftsLastMonth.find(s => { return s.weekNr === weekNr && @@ -536,6 +967,111 @@ function getShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) { }); } +//ToDo use bulk find instead of loop +// async function getAvailablePublishersForShift(startTime, endTime, allPublishers, publishersThisWeek) { +// let availablePublishers = []; + +// for (let publisher of allPublishers) { +// let availability = await FindPublisherAvailability(publisher.id, startTime, endTime); + +// if (availability && !publishersThisWeek.includes(publisher.id)) { +// availablePublishers.push(publisher); +// } +// } + +// return availablePublishers; +// } + +async function getAvailablePublishersForShiftNew(startTime, endTime, allPublishers, publishersThisWeek) { + let availablePublishers = []; + //let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', startTime, true, false, false, true, false); + for (let publisher of allPublishers) { + const isAvailableForShift = publisher.availabilities.some(avail => + avail.startTime <= startTime + && avail.endTime >= endTime + ); + + if (isAvailableForShift && !publishersThisWeek.includes(publisher.id)) { + availablePublishers.push(publisher); + } + } + + return availablePublishers; +} + +async function FindPublisherAvailability(publisherId, startDate, endDate, dayOfWeekEnum, weekNr) { + const prisma = common.getPrismaClient(); + const start = new Date(startDate); + const end = new Date(endDate); + const hours = start.getHours(); + const minutes = start.getMinutes(); + + const exactAvailabilities = await prisma.availability.findMany({ + where: { + publisherId: publisherId, + // type: AvailabilityType.OneTime, + AND: [ // Ensure both conditions must be met + { startTime: { lte: start } }, // startTime is less than or equal to the date + { endTime: { gte: end } },// endTime is greater than or equal to the date + ], + } + }); + + + // Query for repeating availabilities, ignoring exact date, focusing on time and day of week/month + let repeatingAvailabilities = await prisma.availability.findMany({ + where: { + publisherId: publisherId, + dayOfMonth: null, // This signifies a repeating availability + OR: [ + { dayofweek: dayOfWeekEnum },// Matches the specific day of the week + { weekOfMonth: weekNr } // Matches specific weeks of the month + ] + } + }); + //filter out availabilities that does not match the time + // repeatingAvailabilities = repeatingAvailabilities.filter(avail => { + // return avail.startTime.getHours() <= hours && avail.endTime.getHours() >= hours + // && avail.startTime.getMinutes() <= minutes && avail.endTime.getMinutes() >= minutes + // && avail.startTime <= new Date(startDate) && (endDate ? avail.endTime >= new Date(endDate) : true) + // }); + + repeatingAvailabilities = repeatingAvailabilities.filter(avail => { + const availStart = new Date(avail.startTime); + const availEnd = new Date(avail.endTime); + const availUntil = avail.endDate ? new Date(avail.endDate) : null; + + const availStartTimeInt = common.timeToInteger(availStart.getHours(), availStart.getMinutes()); + const availEndTimeInt = common.timeToInteger(availEnd.getHours(), availEnd.getMinutes()); + const startTimeInt = common.timeToInteger(start.getHours(), start.getMinutes()); + const endTimeInt = common.timeToInteger(end.getHours(), end.getMinutes()); + + const isValid = availStartTimeInt <= startTimeInt && availEndTimeInt >= endTimeInt + && availStart <= start + && (!availUntil || availUntil >= end); + + return isValid; + }); + + // return [...exactAvailabilities, ...repeatingAvailabilities]; + // Combine the exact and repeating availabilities, return first or null if no availabilities are found + return exactAvailabilities.length > 0 ? exactAvailabilities[0] : repeatingAvailabilities.length > 0 ? repeatingAvailabilities[0] : null; +} + +// ### COPIED TO shift api (--) ### + +// function addAssignmentToPublisher(shiftAssignments: any[], publisher: Publisher) { +// shiftAssignments.push({ publisherId: publisher.id }); +// publisher.currentWeekAssignments++ || 1; +// publisher.currentDayAssignments++ || 1; +// publisher.currentMonthAssignments++ || 1; +// //console.log(`manual assignment: ${dayName} ${dayOfM} ${shiftStart}:${shiftEnd} ${p.firstName} ${p.lastName} ${p.availabilityIndex} ${p.currentMonthAssignments}`); +// console.log(`manual assignment: ${publisher.firstName} ${publisher.lastName} ${publisher.currentMonthAssignments}`); +// return publisher; +// } + + + /** * Dangerous function that deletes all shifts and publishers. * @param date @@ -546,10 +1082,10 @@ async function DeleteSchedule(axios: Axios, date: Date, forDay: Boolean | undefi let monthInfo = common.getMonthDatesInfo(new Date(date)); if (forDay) { // Delete shifts only for the specific day - await DeleteShiftsForDay(monthInfo.date); + await data.DeleteShiftsForDay(monthInfo.date); } else { // Delete all shifts for the entire month - await DeleteShiftsForMonth(monthInfo); + await data.DeleteShiftsForMonth(monthInfo); } } catch (error) { @@ -587,103 +1123,6 @@ async function ImportShiftsFromDocx(axios: Axios) { } } -/** - * Retrieves shifts and publishers for the previous months based on the given month information. - * @deprecated This function is deprecated and will be removed in future versions. Use `filterPublishers` from `/pages/api/data/index.ts` instead. - * @param monthInfo - An object containing information about the last month, including its first day and last Sunday. - * @returns A Promise that resolves to an array containing the publishers for the previous months. - */ -// async function getShiftsAndPublishersForPreviousMonths(monthInfo: { firstDay: any; lastSunday: any; firstMonday: any; nrOfWeeks: number; }) { -// const prisma = common.getPrismaClient(); //old: (global as any).prisma; - - -// const [shiftsLastMonth, initialPublishers] = await Promise.all([ -// prisma.shift.findMany({ -// where: { -// startTime: { -// gte: monthInfo.firstDay, -// lte: monthInfo.lastSunday, -// }, -// }, -// include: { -// assignments: { -// include: { -// publisher: true, -// }, -// }, -// }, -// }), - -// prisma.publisher.findMany({ -// where: { -// isActive: true, -// }, -// include: { -// availabilities: { -// where: { -// isActive: true, -// }, -// }, -// assignments: { -// include: { -// shift: true, -// }, -// }, -// }, -// }), -// ]); - -// // Group shifts by day -// function getDayFromDate(date: Date) { -// return date.toISO String().split('T')[0]; -// } -// const groupedShifts = shiftsLastMonth.reduce((acc: { [x: string]: any[]; }, shift: { startTime: string | number | Date; }) => { -// const day = getDayFromDate(new Date(shift.startTime)); -// if (!acc[day]) { -// acc[day] = []; -// } -// acc[day].push(shift); -// return acc; -// }, {}); - -// //temp fix - calculate shift.weekNr -// const updatedShiftsLastMonth = []; -// for (const day in groupedShifts) { -// const shifts = groupedShifts[day]; -// for (let i = 0; i < shifts.length; i++) { -// const shift = shifts[i]; -// updatedShiftsLastMonth.push({ -// ...shift, -// weekNr: common.getWeekNumber(shift.startTime) + 1, -// shiftNr: i + 1 // The shift number for the day starts from 1 -// }); -// } -// } -// const publishers = initialPublishers.map((publisher: { assignments: any[]; desiredShiftsPerMonth: number; }) => { -// // const lastMonthStartDate = new Date(date.getFullYear(), date.getMonth() - 1, 1); -// // const last2MonthsStartDate = new Date(date.getFullYear(), date.getMonth() - 2, 1); - -// const filterAssignmentsByDate = (startDate: any, endDate: any) => -// publisher.assignments.filter((assignment: { shift: { startTime: string | number | Date; }; }) => isDateBetween(new Date(assignment.shift.startTime), startDate, endDate)); - -// const lastMonthAssignments = filterAssignmentsByDate(monthInfo.firstMonday, monthInfo.lastSunday); -// //const last2MonthsAssignments = filterAssignmentsByDate(last2MonthsStartDate, monthInfo.firstMonday); - -// const desiredShifts = publisher.desiredShiftsPerMonth * (monthInfo.nrOfWeeks / 4); -// const availabilityIndex = Math.round((lastMonthAssignments.length / desiredShifts) * 100) / 100; - -// return { -// ...publisher, -// availabilityIndex, -// currentWeekAssignments: 0, -// currentMonthAssignments: 0, -// assignmentsLastMonth: lastMonthAssignments.length, -// //assignmentsLast2Months: last2MonthsAssignments.length, -// }; -// }); - -// return [updatedShiftsLastMonth, publishers]; -// } // ********************************************************************************************************************* //region helpers diff --git a/src/helpers/common.js b/src/helpers/common.js index 192a273..dce0164 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -363,7 +363,11 @@ exports.getDateFormated = function (date) { return `${dayOfWeekName} ${day} ${monthName} ${year} г.`; } -exports.getDateFormatedShort = function (date) { +exports.getDateFormattedShort = function (date) { + if (!date) { + return ""; + } + const day = date.getDate(); const monthName = exports.getMonthName(date.getMonth()); return `${day} ${monthName}`; @@ -592,8 +596,13 @@ exports.setTime = (baseDateTime, timeDateTime) => { }); }; +exports.timeToInteger = (hours, minutes) => { + return hours * 100 + minutes; +} + // Format date to a specified format, defaulting to 'HH:mm' exports.getTimeFormatted = (input, format = 'HH:mm') => { + if (!input) return ""; const dateTime = parseDate(input); return dateTime.toFormat(format); }; @@ -695,6 +704,16 @@ exports.parseBool = function (value) { return truthyValues.includes(String(value).toLowerCase()); } +exports.getStartOfDay = function (date) { + const result = new Date(date); // create a copy of the input date + result.setHours(0, 0, 0, 0); // set time to midnight + return result; +} +exports.getEndOfDay = function (date) { + const result = new Date(date); + result.setHours(23, 59, 59, 999); // set time to the last millisecond of the day + return result; +} exports.getStartOfWeek = function (date) { const result = new Date(date); // create a copy of the input date diff --git a/src/helpers/data.js b/src/helpers/data.js index fdad7a1..ad3f5d8 100644 --- a/src/helpers/data.js +++ b/src/helpers/data.js @@ -1,5 +1,9 @@ const common = require('./common'); +// const { Prisma, PrismaClient, Publisher, Shift, DayOfWeek } = require("@prisma/client"); +// or +const DayOfWeek = require("@prisma/client").DayOfWeek; + async function findPublisher(names, email, select, getAll = false) { // Normalize and split the name if provided @@ -78,53 +82,28 @@ async function findPublisher(names, email, select, getAll = false) { } } -async function findPublisherAvailability(publisherId, date) { - const prisma = common.getPrismaClient(); - date = new Date(date); // Convert to date object if not already - const hours = date.getHours(); - const minutes = date.getMinutes(); - const potentialAvailabilities = await prisma.availability.findMany({ - where: { - publisherId: publisherId, - AND: [ // Ensure both conditions must be met - { - startTime: { - lte: new Date(date), // startTime is less than or equal to the date - }, - }, - { - endTime: { - gte: new Date(date), // endTime is greater than or equal to the date - }, - }, - ], - } - }); +//# new - to verify +// should be equivalent to the following prisma filer +// whereClause["availabilities"] = { +// some: { +// OR: [ +// // Check if dayOfMonth is set and filterDate is between start and end dates (Assignments on specific days AND time) +// { +// dayOfMonth: filterDate.getDate(), +// startTime: { lte: filterDate }, +// endTime: { gte: filterDate } +// }, +// // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) +// { +// dayOfMonth: null, +// dayofweek: dayOfWeekEnum, +// startTime: { gte: filterDate }, +// } +// ] +// } +// }; - if (potentialAvailabilities.length === 0) { - return null; // No availability found - } - // Filter the results based on time and other criteria when not exact date match - const availability = potentialAvailabilities.find(avail => { - const availStartHours = avail.startTime.getHours(); - const availStartMinutes = avail.startTime.getMinutes(); - const availEndHours = avail.endTime.getHours(); - const availEndMinutes = avail.endTime.getMinutes(); - - const isAfterStartTime = hours > availStartHours || (hours === availStartHours && minutes >= availStartMinutes); - const isBeforeEndTime = hours < availEndHours || (hours === availEndHours && minutes <= availEndMinutes); - // check day of week if not null - const isCorrectDayOfWeek = avail.repeatWeekly ? avail.startTime.getDay() === date.getDay() : true; - const isExactDateMatch = avail.dayOfMonth ? avail.startTime.toDateString() === date.toDateString() : true; - const isBeforeEndDate = avail.repeatWeekly ? true : avail.endTime > date; - //const isCorrectWeekOfMonth = avail.repeatWeekly ? true : avail.weekOfMonth === weekOfMonth; - - return isAfterStartTime && isBeforeEndTime && isCorrectDayOfWeek && isExactDateMatch && isBeforeEndDate; - }); - - return availability; -} async function getAvailabilities(userId) { @@ -227,7 +206,21 @@ async function getAvailabilities(userId) { } - +/** + * Filters publishers based on various criteria including exact times, monthly duration, + * and whether or not to include statistics about publishers' availabilities and assignments. + * This function heavily relies on the `prisma` client to query and manipulate data related to publishers. + * + * @param {Array|string} selectFields - Fields to select from the publishers data. Can be an array of field names or a comma-separated string of field names. + * @param {string|Date} filterDate - The reference date for filtering. Can be a date string or a Date object. Used to determine relevant time frames like current month, previous month, etc. + * @param {boolean} [isExactTime=false] - If true, filters publishers who are available at the exact time of `filterDate` plus/minus a specific duration (e.g., 90 minutes). + * @param {boolean} [isForTheMonth=false] - If true, adjusts the filtering to encompass the entire month based on `filterDate`. + * @param {boolean} [noEndDateFilter=false] - If true, removes any filtering based on the end date of publishers' availabilities. + * @param {boolean} [isWithStats=true] - If true, includes statistical data about publishers' availabilities and assignments in the output. + * @param {boolean} [includeOldAvailabilities=false] - If true, includes publishers' previous availabilities in the calculations and output. + * + * @returns {Promise} Returns a promise that resolves to an array of publishers with filtered data according to the specified criteria. + */ async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false) { const prisma = common.getPrismaClient(); @@ -347,7 +340,7 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false } console.log(`getting publishers for date: ${filterDate}, isExactTime: ${isExactTime}, isForTheMonth: ${isForTheMonth}`); - console.log`whereClause: ${JSON.stringify(whereClause)}` + //console.log`whereClause: ${JSON.stringify(whereClause)}` //include availabilities if flag is true let publishers = await prisma.publisher.findMany({ where: whereClause, @@ -357,8 +350,9 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false } }); - console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`); + ///console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`); + // include repeating weekly availabilities. generate occurrences for the month // convert matching weekly availabilities to availabilities for the day to make further processing easier on the client. // we trust that the filtering was OK, so we use the dateFilter as date. publishers.forEach(pub => { @@ -448,16 +442,16 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false } }); - - + // ToDo: test case/unit test + // ToDo: check and validate the filtering and calculations if (isExactTime) { //HERE WE FILTER by time for repeating availabilities. We can't do that if we don't have // whereClause["availabilities"].some.OR[1].startTime = { gte: filterTimeFrom }; // whereClause["availabilities"].some.OR[1].endTime = { gte: filterTimeTo } publishers.forEach(pub => { - pub.availabilities.filter(a => a.startTime > filterTimeFrom && a.endTime < filterTimeTo) + pub.availabilities = pub.availabilities.filter(a => a.startTime <= filterTimeFrom && a.endTime >= filterTimeTo); }); - publishers.filter(pub => pub.availabilities.length > 0); + publishers = publishers.filter(pub => pub.availabilities.length > 0); } // if (isExactTime) { @@ -472,10 +466,10 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false } //ToDo: refactor this function -async function getAllPublishersWithStatistics(filterDate, noEndDateFilter = false) { +async function getAllPublishersWithStatisticsMonth(filterDateDuringMonth, noEndDateFilter = false, includeOldAvailabilities = true) { const prisma = common.getPrismaClient(); - const monthInfo = common.getMonthDatesInfo(new Date(filterDate)); + const monthInfo = common.getMonthDatesInfo(new Date(filterDateDuringMonth)); const dateStr = new Date(monthInfo.firstMonday).toISOString().split('T')[0]; let publishers = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', dateStr, false, true, noEndDateFilter, true, true); @@ -787,6 +781,393 @@ async function getCoverMePublisherEmails(shiftId) { return { shift, availablePublishers: availablePublishers, subscribedPublishers }; } +// ### COPIED TO shift api (++) ### + +/** JSDoc + * Generates a schedule. + * +0. generate shifts and assign publishers from the previous month if still available +1. Make sure we always put people only when they are available. +2. First provision one male or two females that are available for transport in the first and last shifts. +3, Then gradually fill all other shifts with day by day troughout the whole month (monthInfo.firstMonday to .lastSunday) with first one, then two, then 3 and wherever possible more (up to CartEvent.numberOfPublishers number) +4. Some publishers are available only at specific time (somoetimes only once) and other are more available. if people are available only for this time, prioritize them so they are not left behind. +5. prioritize based on publisher's desiredShiftsPerMonth and previous months assignments. +6. Idealy noone should be more than once a week. disqualify publishers already on a shift this week. only assign them if there are no other options and we have less than 3 publishers on a specific shift. + * + * @param {Axios} axios Axios instance for making requests. + * @param {string} date The date for the schedule. + * @param {boolean} [copyFromPreviousMonth=false] Whether to copy from the previous month. + * @param {boolean} [autoFill=false] Whether to autofill data. + * @param {boolean} forDay Specific day flag. + */ +async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) { + let missingPublishers = []; + let publishersWithChangedPref = []; + + 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 getAllPublishersWithStatisticsMonth('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', date, false, true, false, true, true); + + let shiftAssignments = []; + let day = new Date(monthInfo.firstMonday); + let endDate = monthInfo.lastSunday; + let dayNr = 1; + let weekNr = 1; + + if (forDay) { + day = monthInfo.date; + endDate.setDate(monthInfo.date.getDate() + 1); + dayNr = monthInfo.date.getDate(); + weekNr = common.getWeekNumber(monthInfo.date); + } + + let publishersThisWeek = []; + + // 0. generate shifts and assign publishers from the previous month if still available + while (day < endDate) { + let availablePubsForTheDay = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true); + console.log("passing schedule generation for " + day.toLocaleDateString()); + const dayOfM = day.getDate(); + let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day); + let dayName = common.DaysOfWeekArray[day.getDayEuropean()]; + const event = events.find((event) => event.dayofweek == dayName && (event.dayOfMonth == null || event.dayOfMonth == dayOfM)); + + if (!event) { + day.setDate(day.getDate() + 1); + continue; + } + + event.startTime = new Date(event.startTime); + event.endTime = new Date(event.endTime); + + let startTime = new Date(day); + startTime.setHours(event.startTime.getHours()); + startTime.setMinutes(event.startTime.getMinutes()); + let endTime = new Date(day); + endTime.setHours(event.endTime.getHours()); + endTime.setMinutes(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++; + 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(); + + const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr); + + if (shiftLastMonthSameDay) { + for (let assignment of shiftLastMonthSameDay.assignments) { + let publisher = assignment.publisher; + console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName); + let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr); + console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime)); + + if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) { + shiftAssignments.push({ + publisherId: publisher.id, + isConfirmed: true, + isWithTransportIn: availability.isWithTransportIn, + isWithTransportOut: availability.isWithTransportOut + }); + publishersThisWeek.push(publisher.id); + } + } + } + + + let publishersNeeded = event.numberOfPublishers - shiftAssignments.length; + //ToDo: check if getAvailablePublishersForShift is working correctly + let availablePublishers = await getAvailablePublishersForShift(shiftStart, shiftEnd, publishers, publishersThisWeek); + + console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length); + + // Prioritize publishers with minimal availability + // SKIP ADDING PUBLISHERS FOR NOW + // availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); + + // for (let i = 0; i < publishersNeeded; i++) { + // if (availablePublishers[i]) { + // shiftAssignments.push({ publisherId: availablePublishers[i].id }); + // publishersThisWeek.push(availablePublishers[i].id); + // } + // } + + 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, + isBySystem: true, + }; + }), + }, + }, + }); + + shiftStart = new Date(shiftEnd); + shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration); + } + + day.setDate(day.getDate() + 1); + dayNr++; + if (common.DaysOfWeekArray[day.getDayEuropean()] === DayOfWeek.Sunday) { + weekNr++; + publishersThisWeek = []; + publishers.forEach(p => p.currentWeekAssignments = 0); + } + if (forDay) break; + } + + let allShifts = await prisma.shift.findMany({ + where: { + startTime: { + gte: monthInfo.firstMonday, + lt: monthInfo.lastSunday, + }, + }, + include: { + assignments: { + include: { + publisher: true, + }, + }, + }, + }); + + console.log(" second pass " + monthInfo.monthName + " " + monthInfo.year); + // 2. First pass - prioritize shifts with transport where it is needed + day = monthInfo.firstMonday; + dayNr = 1; + weekNr = 1; + while (day < endDate) { + let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day); + let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]); + if (event) { + let availablePubsForTheDay = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true); + + let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day)); + let transportShifts = shifts.filter(s => s.requiresTransport); + transportShifts.forEach(shift => { + let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id)); + availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); + let publishersNeeded = event.numberOfPublishers - shift.assignments.length; + if (publishersNeeded > 0) {//get the beset match + if (availablePublishers[0]) { + shift.assignments.push({ publisherId: availablePublishers[i].id }); + } + + } + }); + // 3. Second pass - fill the rest of the shifts + let shiftsToFill = shifts.filter(s => !s.requiresTransport); + shiftsToFill.forEach(shift => { + let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id)); + availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); + let publishersNeeded = event.numberOfPublishers - shift.assignments.length; + if (publishersNeeded > 0) {//get the beset match + if (availablePublishers[0]) { + shift.assignments.push({ publisherId: availablePublishers[i].id }); + } + + } + }); + } + + day.setDate(day.getDate() + 1); + } + + if (!forDay) { + console.log("###############################################"); + console.log(" DONE CREATING SCHEDULE FOR " + monthInfo.monthName + " " + monthInfo.year); + console.log("###############################################"); + } + + return {}; + } catch (error) { + console.log(error); + return { error: error }; + } +} + +async function DeleteShiftsForMonth(monthInfo) { + try { + const prisma = common.getPrismaClient(); + await prisma.shift.deleteMany({ + where: { + startTime: { + gte: monthInfo.firstMonday, + lt: monthInfo.lastSunday, + }, + }, + }); + } catch (e) { + console.log(e); + } +} + +async function DeleteShiftsForDay(date) { + const prisma = common.getPrismaClient(); + try { + // Assuming shifts do not span multiple days, so equality comparison is used + await prisma.shift.deleteMany({ + where: { + startTime: { + gte: date, + lt: new Date(date.getTime() + 86400000), // +1 day in milliseconds + }, + }, + }); + } catch (e) { + console.log(e); + } +} + +async function getShiftsFromLastMonth(monthInfo) { + const prisma = common.getPrismaClient(); + // Fetch shifts for the month + const rawShifts = await prisma.shift.findMany({ + where: { + startTime: { + gte: monthInfo.firstMonday, + lte: monthInfo.lastSunday, + }, + }, + include: { + assignments: { + include: { + publisher: true, + }, + }, + }, + }); + + // Process shifts to add weekNr and shiftNr + return rawShifts.map(shift => ({ + ...shift, + weekNr: common.getWeekNumber(new Date(shift.startTime)), + shiftNr: rawShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(shift.startTime)).indexOf(shift) + 1, + weekDay: common.DaysOfWeekArray[new Date(shift.startTime).getDayEuropean()], + })); +} +function findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) { + let weekDay = common.DaysOfWeekArray[day.getDayEuropean()]; + return shiftsLastMonth.find(s => { + return s.weekNr === weekNr && + s.shiftNr === shiftNr && + s.weekDay === weekDay; + }); +} + +//ToDo use bulk find instead of loop +async function getAvailablePublishersForShift(startTime, endTime, allPublishers, publishersThisWeek) { + let availablePublishers = []; + + for (let publisher of allPublishers) { + let availability = await FindPublisherAvailability(publisher.id, startTime, endTime); + + if (availability && !publishersThisWeek.includes(publisher.id)) { + availablePublishers.push(publisher); + } + } + + return availablePublishers; +} + +async function FindPublisherAvailability(publisherId, startDate, endDate, dayOfWeekEnum, weekNr) { + const prisma = common.getPrismaClient(); + const start = new Date(startDate); + const end = new Date(endDate); + const hours = start.getHours(); + const minutes = start.getMinutes(); + + const exactAvailabilities = await prisma.availability.findMany({ + where: { + publisherId: publisherId, + // type: AvailabilityType.OneTime, + AND: [ // Ensure both conditions must be met + { startTime: { lte: start } }, // startTime is less than or equal to the date + { endTime: { gte: end } },// endTime is greater than or equal to the date + ], + } + }); + + + // Query for repeating availabilities, ignoring exact date, focusing on time and day of week/month + let repeatingAvailabilities = await prisma.availability.findMany({ + where: { + publisherId: publisherId, + dayOfMonth: null, // This signifies a repeating availability + OR: [ + { dayofweek: dayOfWeekEnum },// Matches the specific day of the week + { weekOfMonth: weekNr } // Matches specific weeks of the month + ] + } + }); + //filter out availabilities that does not match the time + // repeatingAvailabilities = repeatingAvailabilities.filter(avail => { + // return avail.startTime.getHours() <= hours && avail.endTime.getHours() >= hours + // && avail.startTime.getMinutes() <= minutes && avail.endTime.getMinutes() >= minutes + // && avail.startTime <= new Date(startDate) && (endDate ? avail.endTime >= new Date(endDate) : true) + // }); + + repeatingAvailabilities = repeatingAvailabilities.filter(avail => { + const availStart = new Date(avail.startTime); + const availEnd = new Date(avail.endTime); + const availUntil = avail.endDate ? new Date(avail.endDate) : null; + + const availStartTimeInt = common.timeToInteger(availStart.getHours(), availStart.getMinutes()); + const availEndTimeInt = common.timeToInteger(availEnd.getHours(), availEnd.getMinutes()); + const startTimeInt = common.timeToInteger(start.getHours(), start.getMinutes()); + const endTimeInt = common.timeToInteger(end.getHours(), end.getMinutes()); + + const isValid = availStartTimeInt <= startTimeInt && availEndTimeInt >= endTimeInt + && availStart <= start + && (!availUntil || availUntil >= end); + + return isValid; + }); + + // return [...exactAvailabilities, ...repeatingAvailabilities]; + // Combine the exact and repeating availabilities, return first or null if no availabilities are found + return exactAvailabilities.length > 0 ? exactAvailabilities[0] : repeatingAvailabilities.length > 0 ? repeatingAvailabilities[0] : null; +} + +// ### COPIED TO shift api (--) ### + + // function matchesAvailability(avail, filterDate) { // // Setting the start and end time of the filterDate // filterDate.setHours(0, 0, 0, 0); @@ -824,11 +1205,14 @@ async function runSqlFile(filePath) { module.exports = { findPublisher, - findPublisherAvailability, + FindPublisherAvailability, runSqlFile, getAvailabilities, filterPublishersNew, getCoverMePublisherEmails, - getAllPublishersWithStatistics, - getCalendarEvents + getAllPublishersWithStatisticsMonth, + getCalendarEvents, + GenerateSchedule, + DeleteShiftsForMonth, + DeleteShiftsForDay, }; \ No newline at end of file