Files
mwitnessing/pages/api/shiftgenerate.ts
2024-06-26 10:13:53 +03:00

877 lines
34 KiB
TypeScript

//import { getToken } from "next-auth/jwt";
import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt";
import { set, format, addDays } from 'date-fns';
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 date = req.query.date?.toString() || common.getISODateOnly(new Date());
var copyFromPreviousMonth = common.parseBool(req.query.copyFromPreviousMonth);
var autoFill = common.parseBool(req.query.autoFill);
var forDay = common.parseBool(req.query.forDay);
var type = parseInt(req.query.type) || 0;
if (type == 2) {
var result = await GenerateOptimalSchedule(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
}
else {
var result = await GenerateSchedule(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
}
res.send(JSON.stringify(result?.error?.toString()));
break;
case "delete":
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;
}
}
// ### COPIED TO shift api (++) ###
let scheduledPubsPerDayAndWeek = {};
// Function to flatten the registry
// Function to update the registry
function updateRegistry(publisherId, day, weekNr) {
// Registry schema: {day: {weekNr: [publisherIds]}}
const dayKey = common.getISODateOnly(day);
if (!scheduledPubsPerDayAndWeek[dayKey]) {
scheduledPubsPerDayAndWeek[dayKey] = {};
}
if (!scheduledPubsPerDayAndWeek[dayKey][weekNr]) {
scheduledPubsPerDayAndWeek[dayKey][weekNr] = [];
}
scheduledPubsPerDayAndWeek[dayKey][weekNr].push(publisherId);
}
function flattenRegistry(dayKey) {
const weekEntries = scheduledPubsPerDayAndWeek[dayKey] || {};
return Object.values(weekEntries).flat();
}
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0) {
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.originalPublisher ?? assignment.publisher;
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName + assignment.originalPublisher ? " (original)" : "");
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);
updateRegistry(publisher.id, day, weekNr);
}
}
}
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);
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 = [];
let rankedPublishers = [];
// Second 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 = 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) {
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);
if (algType == 0) {
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
} else if (algType == 1) {
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
}
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);
updateRegistry(rankedPublishers[0].id, day, weekNr);
}
}
}
}
day.setDate(day.getDate() + 1);
}
// Fill the rest of the shifts
let goal = 1;
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 = 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) {
console.log("Filling shift " + shift.name + " with " + goal + " publishers");
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);
if (algType == 0) {
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
} else if (algType == 1) {
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
}
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);
updateRegistry(rankedPublishers[0].id, day, weekNr);
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);
updateRegistry(familyMember.id, day, weekNr);
}
});
}
}
}
}
}
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;
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 RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek, currentDay) {
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 desiredCompletionA = a.currentMonthAssignments / a.desiredShiftsPerMonth;
const desiredCompletionB = b.currentMonthAssignments / b.desiredShiftsPerMonth;
let adjustedCompletionA = desiredCompletionA;
let adjustedCompletionB = desiredCompletionB;
// Apply penalties based on proximity to current day
for (let i = 1; i <= 6; i++) {
const previousDayKey = common.getISODateOnly(addDays(currentDay, -i));
const nextDayKey = common.getISODateOnly(addDays(currentDay, i));
const penalty = [0.5, 0.7, 0.8, 0.85, 0.9, 0.95][i - 1]; // Penalties for +-1 to +-6 days
if (flattenRegistry(previousDayKey).includes(a.id)) {
adjustedCompletionA *= penalty;
}
if (flattenRegistry(previousDayKey).includes(b.id)) {
adjustedCompletionB *= penalty;
}
if (flattenRegistry(nextDayKey).includes(a.id)) {
adjustedCompletionA *= penalty;
}
if (flattenRegistry(nextDayKey).includes(b.id)) {
adjustedCompletionB *= penalty;
}
}
const desiredCompletionDiff = adjustedCompletionA - adjustedCompletionB;
if (desiredCompletionDiff !== 0) return desiredCompletionDiff;
// 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, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
// 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;
});
const calculateScoreAndPenalties = (p) => {
let score = (p.isMale ? weights.gender : 0) -
(p.desiredCompletion * weights.desiredCompletion) +
((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
(p.currentMonthAssignments * weights.lastMonthCompletion) -
(p.currentMonthAssignments * weights.currentAssignments);
let penalties = [];
// Apply penalties based on proximity to current day
for (let i = 1; i <= 6; i++) {
const previousDayKey = common.getISODateOnly(addDays(currentDay, -i));
const nextDayKey = common.getISODateOnly(addDays(currentDay, i));
const penalty = [0.5, 0.7, 0.8, 0.85, 0.9, 0.95][i - 1]; // Penalties for +-1 to +-6 days
if (flattenRegistry(previousDayKey).includes(p.id)) {
score *= penalty;
penalties.push({ day: previousDayKey, penalty });
}
if (flattenRegistry(nextDayKey).includes(p.id)) {
score *= penalty;
penalties.push({ day: nextDayKey, penalty });
}
}
return { score, penalties };
};
// Calculate scores and penalties for each publisher
publishers.forEach(p => {
const result = calculateScoreAndPenalties(p);
p.score = result.score;
p.penalties = result.penalties;
});
// Sort publishers based on score
let ranked = publishers.sort((a, b) => b.score - a.score);
// Log the scores and penalties of the top publisher
if (ranked.length > 0) {
console.log(`Top Publisher: ${ranked[0].firstName} ${ranked[0].lastName}`,
` Score: ${ranked[0].score}`, "last score: ", ranked[ranked.length - 1].score,
` Penalties: `, ranked[0].penalties);
}
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
// *********************************************************************************************************************