changes to auto shift generation

This commit is contained in:
Dobromir Popov
2024-06-26 20:12:25 +03:00
parent 2cd50bd34f
commit 85be661205
3 changed files with 687 additions and 453 deletions

View File

@ -1,9 +1,12 @@
//import { getToken } from "next-auth/jwt";
const fs = require('fs');
const path = require('path');
import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt";
import { set, format, addDays } from 'date-fns';
import { set, format, isBefore, addDays, addMinutes, isAfter, isEqual, getHours, getMinutes, getSeconds } from 'date-fns';
import type { NextApiRequest, NextApiResponse } from "next";
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
@ -58,10 +61,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
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);
// var result = await GenerateOptimalSchedule(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
var result = await GenerateScheduleNew(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
}
else {
// Create a new log file with a unique name
const logFileName = `shift_generate_log_${Date.now()}.txt`;
const logFilePath = path.join(process.cwd(), logFileName);
// Override console.log to write to the log file
const originalConsoleLog = console.log;
console.log = function (message) {
fs.appendFileSync(logFilePath, message + '\n');
originalConsoleLog.apply(console, arguments);
};
var result = await GenerateSchedule(axios, date, copyFromPreviousMonth, autoFill, forDay, type);
// Restore the original console.log
console.log = originalConsoleLog;
}
res.send(JSON.stringify(result?.error?.toString()));
break;
@ -136,9 +154,9 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1));
if (forDay) {
await DeleteShiftsForDay(monthInfo.date);
await data.DeleteShiftsForDay(monthInfo.date);
} else {
await DeleteShiftsForMonth(monthInfo);
await data.DeleteShiftsForMonth(monthInfo);
}
const events = await prisma.cartEvent.findMany({
@ -207,20 +225,37 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
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));
let availability = await data.FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr);
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);
if (availability) {
console.log("AVAILABLE: " + publisher.firstName + " " + publisher.lastName + (assignment.originalPublisher ? " (original)" : "") + " - av:" + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime));
} else {
console.log(" " + publisher.firstName + " " + publisher.lastName + (assignment.originalPublisher ? " (original)" : "") + " - NOT AVAILABLE");
}
//---------------------------------------------------
// // COMMENT TO DISABLE COPY FROM LAST MONTH
if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
const transportCount = shiftAssignments.filter(a => a.isWithTransport).length;
const isWithTransport = availability.isWithTransportIn || availability.isWithTransportOut;
if (!isWithTransport || transportCount < 2) {
shiftAssignments.push({
publisherId: publisher.id,
isConfirmed: true,
isBySystem: true,
isWithTransport: isWithTransport
});
publishersThisWeek.push(publisher.id);
updateRegistry(publisher.id, day, weekNr);
}
else {
console.log(" " + publisher.firstName + " " + publisher.lastName + " skipped (transport already assigned)");
}
}
//---------------------------------------------------
}
}
let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
@ -289,7 +324,10 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let rankedPublishers = [];
// Second pass - prioritize shifts with transport where it is needed
console.log(" second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year);
console.log("\r\n\r\n\r\n" + "# ".repeat(50));
console.log("Second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year);
day = new Date(monthInfo.firstMonday);
dayNr = 1;
weekNr = 1;
@ -348,29 +386,30 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
} 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,
},
});
// 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);
}
// shift.assignments.push(newAssignment);
// publishersToday.push(rankedPublishers[0].id);
// updateRegistry(rankedPublishers[0].id, day, weekNr);
// }
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
}
}
}
@ -381,7 +420,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
// Fill the rest of the shifts
let goal = 1;
while (goal <= 4) {
console.log("#".repeat(50));
console.log("\r\n\r\n\r\n" + "# ".repeat(50));
console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year);
day = new Date(monthInfo.firstMonday);
dayNr = 1;
@ -408,7 +447,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let shiftsToFill = shifts.filter(s => s.assignments.length < goal);
console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers");
//when adding new assignment, first check if there are family members available. If yes, olnly proceed adding all available family members at once if there are enough free slots. ignore the main publisher if he/she can]t be with his family and search for the next best candidate
for (const shift of shiftsToFill) {
console.log("Filling shift " + shift.name + " with " + goal + " publishers");
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
@ -417,63 +456,12 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
if (algType == 0) {
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day);
} 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);
}
});
}
}
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
}
}
}
@ -496,12 +484,96 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
}
}
async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheShift, rankedPublishers, publishersToday, day, weekNr) {
if (rankedPublishers.length == 0) {
console.log("No available publishers for shift " + shift.name);
} else {
for (let i = 0; i < rankedPublishers.length; i++) {
let mainPublisher = rankedPublishers[i];
let familyMembers = availablePubsForTheShift.filter(p => (p.familyHeadId && p.familyHeadId === mainPublisher.familyHeadId) || (p.familyHeadId === mainPublisher.id));
if (familyMembers.length > 0 && (shift.assignments.length + familyMembers.length + 1) <= event.numberOfPublishers) {
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and family members to " + new Date(shift.startTime).getDate() + " " + shift.name);
const newAssignment = await prisma.assignment.create({
data: {
shift: {
connect: {
id: shift.id,
},
},
publisher: {
connect: {
id: mainPublisher.id,
},
},
isConfirmed: false,
isBySystem: false,
isWithTransport: shift.requiresTransport,
},
});
shift.assignments.push(newAssignment);
publishersToday.push(mainPublisher.id);
updateRegistry(mainPublisher.id, day, weekNr);
for (const familyMember of familyMembers) {
const newFamilyAssignment = await prisma.assignment.create({
data: {
shift: {
connect: {
id: shift.id,
},
},
publisher: {
connect: {
id: familyMember.id,
},
},
isConfirmed: false,
isBySystem: false,
isWithTransport: shift.requiresTransport,
},
});
shift.assignments.push(newFamilyAssignment);
publishersToday.push(familyMember.id);
updateRegistry(familyMember.id, day, weekNr);
}
break;
} else if (familyMembers.length == 0) {
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " to " + new Date(shift.startTime).getDate() + " " + shift.name);
const newAssignment = await prisma.assignment.create({
data: {
shift: {
connect: {
id: shift.id,
},
},
publisher: {
connect: {
id: mainPublisher.id,
},
},
isConfirmed: false,
isBySystem: false,
isWithTransport: shift.requiresTransport,
},
});
shift.assignments.push(newAssignment);
publishersToday.push(mainPublisher.id);
updateRegistry(mainPublisher.id, day, weekNr);
break;
}
}
}
}
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;
const isAssignedEnough = p.currentMonthAssignments >= p.desiredShiftsPerMonth
|| p.currentMonthAssignments >= 6; // overwrite the desiredShiftsPerMonth to max 6 shifts per month
return isNotAssigned && isNotAssignedToday && !isAssignedEnough;
});
return goodPublishers;
@ -517,7 +589,7 @@ async function FilterInappropriatePublishers(availablePublishers, pubsToExclude,
// 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) {
async function RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek, currentDay: Date) {
publishers.forEach(p => {
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
});
@ -631,11 +703,13 @@ async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAnd
// 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);
const avgScore = (ranked.reduce((acc: number, val: { score: number }) => acc + val.score, 0) / ranked.length).toFixed(2);
const topPublisher = ranked[0];
const lastPublisher = ranked[ranked.length - 1];
console.log(`Top Publisher: ${topPublisher.firstName} ${topPublisher.lastName} (${ranked.length} available)`,
` Score: ${topPublisher.score.toFixed(2)}`, "last score: ", lastPublisher.score.toFixed(2), "Avg: ", avgScore,
` Penalties: `, topPublisher.penalties);
}
return ranked;
}
@ -644,38 +718,7 @@ async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAnd
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();
@ -746,65 +789,6 @@ async function getAvailablePublishersForShiftNew(startTime, endTime, allPublishe
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) {
@ -874,3 +858,251 @@ async function ImportShiftsFromDocx(axios: Axios) {
// *********************************************************************************************************************
//region helpers
// *********************************************************************************************************************
// // *********************************************************************************************************************
// // NEW implementation
// // *********************************************************************************************************************
// async function GenerateScheduleNew(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0) {
// const prisma = common.getPrismaClient();
// const monthInfo = common.getMonthDatesInfo(new Date(date));
// const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1));
// // 0. Generate empty shifts for the whole period
// let shiftPool = await generateEmptyShifts(monthInfo, forDay);
// // 1. Copy shifts from last month if requested
// if (copyFromPreviousMonth) {
// shiftPool = await copyShiftsFromLastMonth(shiftPool, lastMonthInfo);
// }
// // 2. Get all publishers
// const allPublishers = await getAllPublishers(monthInfo);
// // 3. Fill shifts by priority
// await fillShiftsByPriority(shiftPool, allPublishers);
// return { message: "Schedule generated successfully", shifts: shiftPool };
// }
// async function generateEmptyShifts(monthInfo, forDay) {
// const prisma = common.getPrismaClient();
// const startDate = forDay ? monthInfo.date : monthInfo.firstMonday;
// const endDate = forDay ? addDays(monthInfo.date, 1) : monthInfo.lastSunday;
// const events = await prisma.cartEvent.findMany({
// where: { isActive: true }
// });
// let shifts = [];
// for (let day = startDate; day < endDate; day = addDays(day, 1)) {
// const dayOfWeek = common.DaysOfWeekArray[day.getDayEuropean()];
// const event = events.find(e => e.dayofweek === dayOfWeek);
// if (event) {
// let shiftStart = set(day, { hours: event.startTime.getHours(), minutes: event.startTime.getMinutes() });
// const shiftEnd = set(day, { hours: event.endTime.getHours(), minutes: event.endTime.getMinutes() });
// while (shiftStart < shiftEnd) {
// const nextShiftEnd = addMinutes(shiftStart, event.shiftDuration);
// const newShift = await prisma.shift.create({
// data: {
// startTime: shiftStart,
// endTime: nextShiftEnd,
// requiredPublishers: event.numberOfPublishers,
// requiresTransport: shiftStart.getTime() === event.startTime.getTime() || nextShiftEnd.getTime() === event.endTime.getTime(),
// assignments: [],
// weight: 0
// }
// });
// shifts.push(newShift);
// shiftStart = nextShiftEnd;
// }
// }
// }
// return shifts;
// }
// async function copyShiftsFromLastMonth(shiftPool, lastMonthInfo) {
// const prisma = common.getPrismaClient();
// const lastMonthShifts = await prisma.shift.findMany({
// where: {
// startTime: {
// gte: lastMonthInfo.firstMonday,
// lt: lastMonthInfo.lastSunday
// }
// },
// include: {
// assignments: {
// include: {
// publisher: {
// include: {
// availabilities: true
// }
// }
// }
// }
// }
// });
// for (const shift of shiftPool) {
// const matchingLastMonthShift = lastMonthShifts.find(s =>
// s.startTime.getHours() === shift.startTime.getHours() &&
// s.startTime.getMinutes() === shift.startTime.getMinutes() &&
// s.startTime.getDay() === shift.startTime.getDay()
// );
// if (matchingLastMonthShift) {
// for (const assignment of matchingLastMonthShift.assignments) {
// const isStillAvailable = await checkPublisherAvailability(assignment.publisher, shift);
// if (isStillAvailable) {
// shift.assignments.push({
// publisherId: assignment.publisher.id,
// isWithTransport: assignment.isWithTransport,
// isConfirmed: false,
// isBySystem: true
// });
// }
// }
// }
// }
// return shiftPool;
// }
// async function getAllPublishers(monthInfo) {
// const prisma = common.getPrismaClient();
// const publishers = await prisma.publisher.findMany({
// where: { isActive: true },
// include: {
// availabilities: {
// where: {
// startTime: {
// gte: monthInfo.firstMonday,
// lt: monthInfo.lastSunday
// }
// }
// }
// }
// });
// return publishers.map(p => ({
// ...p,
// currentMonthAssignments: 0,
// lastAssignmentDate: null
// }));
// }
// async function fillShiftsByPriority(shiftPool, allPublishers) {
// let remainingShifts = [...shiftPool];
// while (remainingShifts.length > 0) {
// updateShiftWeights(remainingShifts);
// remainingShifts.sort((a, b) => b.weight - a.weight);
// const currentShift = remainingShifts[0];
// const requireTransport = currentShift.requiresTransport && currentShift.assignments.filter(a => a.isWithTransport).length < 2;
// const availablePublishers = await getAvailablePublishersForShift(currentShift, allPublishers, requireTransport);
// const rankedPublishers = rankPublishersForShift(availablePublishers, currentShift, requireTransport);
// if (rankedPublishers.length > 0) {
// const selectedPublisher = rankedPublishers[0];
// await assignPublisherToShift(selectedPublisher, currentShift, requireTransport);
// updatePublisherStats(selectedPublisher, currentShift);
// }
// if (currentShift.assignments.length >= currentShift.requiredPublishers) {
// remainingShifts.shift();
// }
// }
// }
// function updateShiftWeights(shifts) {
// for (const shift of shifts) {
// const remainingSlots = shift.requiredPublishers - shift.assignments.length;
// const transportWeight = shift.requiresTransport && shift.assignments.filter(a => a.isWithTransport).length < 2 ? 10 : 0;
// const timeWeight = shift.startTime.getHours() < 12 ? 5 : 0; // Prioritize morning shifts
// shift.weight = remainingSlots * 10 + transportWeight + timeWeight;
// }
// }
// async function getAvailablePublishersForShift(shift, allPublishers, requireTransport) {
// return allPublishers.filter(publisher => {
// const isAvailable = publisher.availabilities.some(avail =>
// avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
// );
// const isNotAssigned = !shift.assignments.some(a => a.publisherId === publisher.id);
// const hasTransport = requireTransport ? publisher.canProvideTransport : true;
// return isAvailable && isNotAssigned && hasTransport;
// });
// }
// function rankPublishersForShift(publishers, shift, requireTransport) {
// const weights = {
// gender: 2,
// desiredCompletion: 3,
// currentAssignments: 2,
// lastAssignment: 2,
// fairness: 3,
// transport: requireTransport ? 5 : 0
// };
// return publishers.map(p => ({
// ...p,
// score: calculatePublisherScore(p, shift, weights)
// })).sort((a, b) => b.score - a.score);
// }
// function calculatePublisherScore(publisher, shift, weights) {
// const genderScore = publisher.gender === 'male' ? weights.gender : 0;
// const desiredCompletionScore = (publisher.desiredShiftsPerMonth - publisher.currentMonthAssignments) * weights.desiredCompletion;
// const currentAssignmentsScore = -publisher.currentMonthAssignments * weights.currentAssignments;
// const lastAssignmentScore = calculateLastAssignmentScore(publisher, shift) * weights.lastAssignment;
// const fairnessScore = calculateFairnessScore(publisher) * weights.fairness;
// const transportScore = publisher.canProvideTransport ? weights.transport : 0;
// return genderScore + desiredCompletionScore + currentAssignmentsScore + lastAssignmentScore + fairnessScore + transportScore;
// }
// function calculateLastAssignmentScore(publisher, shift) {
// if (!publisher.lastAssignmentDate) return 1;
// const daysSinceLastAssignment = differenceInDays(shift.startTime, publisher.lastAssignmentDate);
// return Math.min(daysSinceLastAssignment / 7, 1); // Normalize to 0-1 range, max out at 7 days
// }
// function calculateFairnessScore(publisher) {
// const utilizationRate = publisher.currentMonthAssignments / publisher.desiredShiftsPerMonth;
// return 1 - utilizationRate; // Higher score for less utilized publishers
// }
// async function assignPublisherToShift(publisher, shift, withTransport) {
// const prisma = common.getPrismaClient();
// const assignment = await prisma.assignment.create({
// data: {
// shift: { connect: { id: shift.id } },
// publisher: { connect: { id: publisher.id } },
// isWithTransport: withTransport,
// isConfirmed: false,
// isBySystem: true,
// },
// });
// shift.assignments.push(assignment);
// }
// function updatePublisherStats(publisher, shift) {
// publisher.currentMonthAssignments++;
// publisher.lastAssignmentDate = shift.startTime;
// }
// async function checkPublisherAvailability(publisher, shift) {
// return publisher.availabilities.some(avail =>
// avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
// );
// }