Files
mwitnessing/pages/api/shiftgenerate.ts
2024-05-26 11:54:22 +03:00

1176 lines
55 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//import { getToken } from "next-auth/jwt";
import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt";
import type { NextApiRequest, NextApiResponse } from "next";
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import { filterPublishers, /* other functions */ } from './index';
import CAL from "../../src/helpers/calendar";
//const common = require("@common");
import common, { logger } from "../../src/helpers/common";
import data from "../../src/helpers/data";
import { Axios } from 'axios';
export default handler;
async function handler(req: NextApiRequest, res: NextApiResponse) {
console.log(req.url);
console.log(req.query);
const prisma = common.getPrismaClient();
// If you don't have the NEXTAUTH_SECRET environment variable set,
// you will have to pass your secret as `secret` to `getToken`
const axios = await axiosServer({ req: req, res: res });
const token = await getToken({ req: req });
if (!token) {
// If no token or invalid token, return unauthorized status
return res.status(401).json({ message: "Unauthorized" });
}
// const token = req.headers.authorization.split('Bearer ')[1]
// const { user } = await verify(token, process.env.NEXTAUTH_SECRET, {
// maxAge: 30 * 24 * 60 * 60, // 30 days
// })
// if (!user.roles.includes('admin')) {
// res.status(401).json({ message: 'Unauthorized' })
// return
// }
// // if (!user.role == "adminer") {
// if (token?.userRole !== "adminer") {
// res.status(401).json({ message: "Unauthorized" });
// console.log("not authorized");
// return;
// }
// var result = { error: "Not authorized" };
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));
res.send(JSON.stringify(result?.error?.toString()));
break;
case "delete":
result = await DeleteSchedule(axios, req.query.date, common.parseBool(req.query.forDay));
let msg = "Deleted schedule for " + (req.query.forDay ? req.query.date : "the entire month of ") + req.query.date + ". Action requested by " + token.email;
logger.warn(msg);
console.log(msg);
res.send("deleted"); // JSON.stringify(result, null, 2)
break;
case "createcalendarevent":
//CAL.GenerateICS();
result = await CreateCalendarForUser(req.query.id);
res.send(result); // JSON.stringify(result, null, 2)
break;
case "test":
var data = prisma.shift.findMany({
where: {
isActive: true
}
});
res.send({
action: "OK",
shifts: data,
locations: prisma.location.findMany({
take: 10, // Limit the number of records to 10
orderBy: {
name: 'asc' // Replace 'someField' with a field you want to sort by
},
})
});
break;
default:
res.send("Invalid action");
break;
}
}
// 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 (++) ###
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 data.getAllPublishersWithStatisticsMonth(date, false, false);
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 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()];
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,
isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut
});
publishersThisWeek.push(publisher.id);
}
}
}
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,
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 }
},
isWithTransport: a.isWithTransport,
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,
},
},
},
});
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 availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', shift.startTime, true, false, false, true, false);
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);
return (hasTransportInAvailability || hasTransportOutAvailability);
});
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
// rank publishers based on different factors
let rankedPublishersOld = await RankPublishersForShift([...availablePublishers])
let rankedPublishers = await RankPublishersForShiftWeighted([...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);
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
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) {
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 FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) {
//ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later.
let goodPublishers = availablePublishers.filter(p => {
const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id);
const isNotAssignedToday = !pubsToExclude.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;
});
return goodPublishers;
}
//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.
//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;
// less assigned first (ascending)
return a.currentMonthAssignments - b.currentMonthAssignments;
});
return ranked;
}
// ToDo: add negative weights for currentweekAssignments, so we avoid assigning the same publishers multiple times in a week. having in mind the days difference between shifts.
async function RankPublishersForShiftWeighted(publishers) {
// Define weights for each criterion
const weights = {
gender: 2,
desiredCompletion: 3,
availability: 2,
lastMonthCompletion: 3,
currentAssignments: 1
};
// Normalize weights to ensure they sum to 1
const totalWeight = Object.values(weights).reduce((acc, val) => acc + val, 0);
Object.keys(weights).forEach(key => {
weights[key] /= totalWeight;
});
publishers.forEach(p => {
p.lastMonthCompletion = p.previousMonthAssignments / p.currentMonthAssignments;
p.desiredCompletion = p.currentMonthAssignments / p.desiredShiftsPerMonth;
});
let ranked = publishers.sort((a, b) => {
// Calculate weighted score for each publisher
const scoreA = (a.isMale ? weights.gender : 0) -
(a.desiredCompletion * weights.desiredCompletion) +
((1 - a.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(a.currentMonthAssignments * weights.lastMonthCompletion) -
(a.currentMonthAssignments * weights.currentAssignments);
const scoreB = (b.isMale ? weights.gender : 0) -
(b.desiredCompletion * weights.desiredCompletion) +
((1 - b.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(b.currentMonthAssignments * weights.lastMonthCompletion) -
(b.currentMonthAssignments * weights.currentAssignments);
return scoreB - scoreA; // Sort descending by score
});
return ranked;
}
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 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
* @returns
*/
async function DeleteSchedule(axios: Axios, date: Date, forDay: Boolean | undefined) {
try {
let monthInfo = common.getMonthDatesInfo(new Date(date));
if (forDay) {
// Delete shifts only for the specific day
await data.DeleteShiftsForDay(monthInfo.date);
} else {
// Delete all shifts for the entire month
await data.DeleteShiftsForMonth(monthInfo);
}
} catch (error) {
console.log(error);
return { error: error };
}
}
async function CreateCalendarForUser(eventId: string | string[] | undefined) {
try {
//CAL.authorizeNew();
CAL.createEvent(eventId);
} catch (error) {
console.log(error);
return { error: error };
}
}
/*
obsolete?
*/
async function ImportShiftsFromDocx(axios: Axios) {
try {
const { data: shifts } = await axios.get(`/api/data/shifts`);
shifts.forEach(async (shift: { id: any; }) => {
await axios.delete(`/api/data/shifts/${shift.id}`);
});
const { data: shiftsToCreate } = await axios.get(`/api/data/shiftsToCreate`);
shiftsToCreate.forEach(async (shift: any) => {
await axios.post(`/api/data/shifts`, shift);
});
} catch (error) {
console.log(error);
return { error: error };
}
}
// *********************************************************************************************************************
//region helpers
// *********************************************************************************************************************