Merge branch 'main' into production
This commit is contained in:
@ -123,25 +123,65 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
// ### COPIED TO shift api (++) ###
|
// ### COPIED TO shift api (++) ###
|
||||||
|
|
||||||
|
let publisherMonthlyAssignments = new Map();
|
||||||
let scheduledPubsPerDayAndWeek = {};
|
let scheduledPubsCacheStatistics = {};
|
||||||
// Function to flatten the registry
|
// Function to flatten the registry
|
||||||
// Function to update the registry
|
// Function to update the registry
|
||||||
function updateRegistry(publisherId, day, weekNr) {
|
function updateRegistry(publisherId, day, weekNr) {
|
||||||
// Registry schema: {day: {weekNr: [publisherIds]}}
|
// Registry schema: {day: {weekNr: [publisherIds]}}
|
||||||
const dayKey = common.getISODateOnly(day);
|
const dayKey = common.getISODateOnly(day);
|
||||||
if (!scheduledPubsPerDayAndWeek[dayKey]) {
|
if (!scheduledPubsCacheStatistics[dayKey]) {
|
||||||
scheduledPubsPerDayAndWeek[dayKey] = {};
|
scheduledPubsCacheStatistics[dayKey] = {};
|
||||||
}
|
}
|
||||||
if (!scheduledPubsPerDayAndWeek[dayKey][weekNr]) {
|
if (!scheduledPubsCacheStatistics[dayKey][weekNr]) {
|
||||||
scheduledPubsPerDayAndWeek[dayKey][weekNr] = [];
|
scheduledPubsCacheStatistics[dayKey][weekNr] = [];
|
||||||
}
|
}
|
||||||
scheduledPubsPerDayAndWeek[dayKey][weekNr].push(publisherId);
|
scheduledPubsCacheStatistics[dayKey][weekNr].push(publisherId);
|
||||||
|
// Update monthly assignments
|
||||||
|
const currentCount = publisherMonthlyAssignments.get(publisherId) || 0;
|
||||||
|
publisherMonthlyAssignments.set(publisherId, currentCount + 1);
|
||||||
}
|
}
|
||||||
function flattenRegistry(dayKey) {
|
function flattenRegistry(dayKey) {
|
||||||
const weekEntries = scheduledPubsPerDayAndWeek[dayKey] || {};
|
const weekEntries = scheduledPubsCacheStatistics[dayKey] || {};
|
||||||
return Object.values(weekEntries).flat();
|
return Object.values(weekEntries).flat();
|
||||||
}
|
}
|
||||||
|
// Function to initialize monthly counts from database
|
||||||
|
async function initializeMonthlyAssignments(prisma, startOfMonth, endOfMonth) {
|
||||||
|
const monthlyAssignments = await prisma.assignment.groupBy({
|
||||||
|
by: ['publisherId'],
|
||||||
|
where: {
|
||||||
|
shift: {
|
||||||
|
startTime: {
|
||||||
|
gte: startOfMonth,
|
||||||
|
lte: endOfMonth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
publisherId: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
publisherMonthlyAssignments = new Map(
|
||||||
|
monthlyAssignments.map(count => [
|
||||||
|
count.publisherId,
|
||||||
|
count._count.publisherId
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Function to get current assignment count for a publisher
|
||||||
|
function getPublisherAssignmentCount(publisherId) {
|
||||||
|
return publisherMonthlyAssignments.get(publisherId) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update publishers with their current counts
|
||||||
|
function updatePublishersWithCurrentCounts(publishers) {
|
||||||
|
return publishers.map(pub => ({
|
||||||
|
...pub,
|
||||||
|
currentMonthAssignments: getPublisherAssignmentCount(pub.id)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0, until) {
|
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay, algType = 0, until) {
|
||||||
|
|
||||||
@ -167,7 +207,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
|
let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
|
||||||
let publishers = await data.getAllPublishersWithStatisticsMonth(date, false, false);
|
let publishers = await data.getAllPublishersWithStatisticsMonth(date, false, false);
|
||||||
|
|
||||||
let shiftAssignments = [];
|
let shiftAssignments: any[] = [];
|
||||||
let day = new Date(monthInfo.firstMonday);
|
let day = new Date(monthInfo.firstMonday);
|
||||||
let endDate = monthInfo.lastSunday;
|
let endDate = monthInfo.lastSunday;
|
||||||
let dayNr = 1;
|
let dayNr = 1;
|
||||||
@ -188,7 +228,12 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
weekNr = common.getWeekNumber(monthInfo.date);
|
weekNr = common.getWeekNumber(monthInfo.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
let publishersThisWeek = [];
|
/**
|
||||||
|
* An array to store the publishers for the current week.
|
||||||
|
*
|
||||||
|
* @type {never[]}
|
||||||
|
*/
|
||||||
|
let publishersThisWeek: never[] = [];
|
||||||
|
|
||||||
|
|
||||||
// # # # # # # # # # # #
|
// # # # # # # # # # # #
|
||||||
@ -242,24 +287,25 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
|
|
||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
// // COMMENT TO DISABLE COPY FROM LAST MONTH
|
// // COMMENT TO DISABLE COPY FROM LAST MONTH
|
||||||
if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
|
// if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
|
||||||
const transportCount = shiftAssignments.filter(a => a.isWithTransport).length;
|
// const transportCount = shiftAssignments.filter(a => a.isWithTransport).length;
|
||||||
const isWithTransport = availability.isWithTransportIn || availability.isWithTransportOut;
|
// const isWithTransport = availability.isWithTransportIn || availability.isWithTransportOut;
|
||||||
|
|
||||||
if (!isWithTransport || transportCount < 2) {
|
// if (!isWithTransport || transportCount < 2) {
|
||||||
shiftAssignments.push({
|
// shiftAssignments.push({
|
||||||
publisherId: publisher.id,
|
// publisherId: publisher.id,
|
||||||
isConfirmed: true,
|
// isConfirmed: true,
|
||||||
isBySystem: true,
|
// isBySystem: true,
|
||||||
isWithTransport: isWithTransport
|
// isWithTransport: isWithTransport
|
||||||
});
|
// });
|
||||||
publishersThisWeek.push(publisher.id);
|
// publishersThisWeek.push(publisher.id);
|
||||||
updateRegistry(publisher.id, day, weekNr);
|
// updateRegistry(publisher.id, day, weekNr);
|
||||||
}
|
// publisher.currentMonthAssignments += 1;
|
||||||
else {
|
// }
|
||||||
console.log(" " + publisher.firstName + " " + publisher.lastName + " skipped (transport already assigned)");
|
// else {
|
||||||
}
|
// console.log(" " + publisher.firstName + " " + publisher.lastName + " skipped (transport already assigned)");
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,7 +404,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
|
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
|
||||||
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
|
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
|
||||||
if (event) {
|
if (event) {
|
||||||
let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false);
|
// 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 shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day));
|
||||||
|
|
||||||
let publishersToday = await prisma.assignment.findMany({
|
let publishersToday = await prisma.assignment.findMany({
|
||||||
@ -394,20 +440,22 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
else if (publishersNeeded > 0) {
|
else if (publishersNeeded > 0) {
|
||||||
console.log("shift " + shift.name + " requires transport (" + transportCapable.length + " transport capable)");
|
console.log("shift " + shift.name + " requires transport (" + transportCapable.length + " transport capable)");
|
||||||
|
|
||||||
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
|
const availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
|
||||||
|
const availablePubsWithCounts = updatePublishersWithCurrentCounts(availablePubsForTheShift);
|
||||||
|
|
||||||
let availablePublishers = availablePubsForTheShift.filter(p => {
|
|
||||||
|
const availablePublishers = availablePubsWithCounts.filter(p => {
|
||||||
const hasTransportInAvailability = shift.transportIn && p.availabilities.some(avail => avail.isWithTransportIn);
|
const hasTransportInAvailability = shift.transportIn && p.availabilities.some(avail => avail.isWithTransportIn);
|
||||||
const hasTransportOutAvailability = shift.transportOut && p.availabilities.some(avail => avail.isWithTransportOut);
|
const hasTransportOutAvailability = shift.transportOut && p.availabilities.some(avail => avail.isWithTransportOut);
|
||||||
|
|
||||||
return (hasTransportInAvailability || hasTransportOutAvailability);
|
return (hasTransportInAvailability || hasTransportOutAvailability);
|
||||||
});
|
});
|
||||||
|
|
||||||
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
|
const appropriatePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift, 10);
|
||||||
if (algType == 0) {
|
if (algType == 0) {
|
||||||
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
rankedPublishers = await RankPublishersForShiftOld([...appropriatePublishers], scheduledPubsCacheStatistics, day);
|
||||||
} else if (algType == 1) {
|
} else if (algType == 1) {
|
||||||
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
rankedPublishers = await RankPublishersForShiftWeighted([...appropriatePublishers], scheduledPubsCacheStatistics, day, weekNr);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
||||||
@ -442,7 +490,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
|
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
|
||||||
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
|
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
|
||||||
if (event) {
|
if (event) {
|
||||||
let availablePubsForTheDay = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, false);
|
//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 shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day));
|
||||||
let publishersToday = await prisma.assignment.findMany({
|
let publishersToday = await prisma.assignment.findMany({
|
||||||
where: {
|
where: {
|
||||||
@ -467,11 +515,11 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
if (publishersNeeded > 0 && shift.assignments.length < goal) {
|
if (publishersNeeded > 0 && shift.assignments.length < goal) {
|
||||||
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
|
let availablePubsForTheShift = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type,familyHeadId', shift.startTime, true, false, false, true, false);
|
||||||
|
|
||||||
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
|
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift, 10);
|
||||||
if (algType == 0) {
|
if (algType == 0) {
|
||||||
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day);
|
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsCacheStatistics, day);
|
||||||
} else if (algType == 1) {
|
} else if (algType == 1) {
|
||||||
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsCacheStatistics, day, weekNr);
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
await AddPublisherAssignment(prisma, event, shift, availablePublishers, rankedPublishers, publishersToday, day, weekNr);
|
||||||
@ -508,10 +556,16 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
|
|||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < rankedPublishers.length; i++) {
|
for (let i = 0; i < rankedPublishers.length; i++) {
|
||||||
let mainPublisher = rankedPublishers[i];
|
let mainPublisher = rankedPublishers[i];
|
||||||
let familyMembers = availablePubsForTheShift.filter(p => (p.id !== mainPublisher.id && (p.id === mainPublisher.familyHeadId) || (p.familyHeadId === mainPublisher.id)));
|
let familyMembers = availablePubsForTheShift.filter(p => {
|
||||||
|
const isNotSelf = p.id !== mainPublisher.id;
|
||||||
|
const isMyFamilyHead = p.id === mainPublisher.familyHeadId;
|
||||||
|
const isMyFamilyMember = p.familyHeadId === mainPublisher.id;
|
||||||
|
|
||||||
|
return isNotSelf && (isMyFamilyHead || isMyFamilyMember);
|
||||||
|
});
|
||||||
|
|
||||||
if (familyMembers.length > 0 && (shift.assignments.length + familyMembers.length + 1) <= event.numberOfPublishers) {
|
if (familyMembers.length > 0 && (shift.assignments.length + familyMembers.length + 1) <= event.numberOfPublishers) {
|
||||||
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and " + familyMembers.length + " available family members to " + new Date(shift.startTime).getDate() + " " + shift.name);
|
console.log("Assigning " + mainPublisher.firstName + " " + mainPublisher.lastName + " and family members: '" + familyMembers.map(fm => `${fm.firstName} ${fm.lastName}`).join(", ") + `' to ${new Date(shift.startTime).getDate()} ${shift.name}`);
|
||||||
|
|
||||||
const hasTransportInAvailability = shift.transportIn && mainPublisher.availabilities.some(avail => avail.isWithTransportIn);
|
const hasTransportInAvailability = shift.transportIn && mainPublisher.availabilities.some(avail => avail.isWithTransportIn);
|
||||||
const hasTransportOutAvailability = shift.transportOut && mainPublisher.availabilities.some(avail => avail.isWithTransportOut);
|
const hasTransportOutAvailability = shift.transportOut && mainPublisher.availabilities.some(avail => avail.isWithTransportOut);
|
||||||
@ -558,6 +612,7 @@ async function AddPublisherAssignment(prisma, event, shift, availablePubsForTheS
|
|||||||
shift.assignments.push(newFamilyAssignment);
|
shift.assignments.push(newFamilyAssignment);
|
||||||
publishersToday.push(familyMember.id);
|
publishersToday.push(familyMember.id);
|
||||||
updateRegistry(familyMember.id, day, weekNr);
|
updateRegistry(familyMember.id, day, weekNr);
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} else if (familyMembers.length == 0) {
|
} else if (familyMembers.length == 0) {
|
||||||
@ -614,7 +669,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.
|
// 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
|
//sort publishers to rank the best option for the current shift assignment
|
||||||
async function RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek, currentDay: Date) {
|
async function RankPublishersForShiftOld(publishers, stats, currentDay: Date) {
|
||||||
publishers.forEach(p => {
|
publishers.forEach(p => {
|
||||||
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
|
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
|
||||||
});
|
});
|
||||||
@ -667,7 +722,7 @@ async function RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek,
|
|||||||
|
|
||||||
|
|
||||||
// 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.
|
// 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) {
|
async function RankPublishersForShiftWeighted(publishers, stats, currentDay, currentWeekNr) {
|
||||||
// Define weights for each criterion
|
// Define weights for each criterion
|
||||||
const weights = {
|
const weights = {
|
||||||
gender: 2,
|
gender: 2,
|
||||||
@ -689,6 +744,9 @@ async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAnd
|
|||||||
});
|
});
|
||||||
|
|
||||||
const calculateScoreAndPenalties = (p) => {
|
const calculateScoreAndPenalties = (p) => {
|
||||||
|
|
||||||
|
// apply for reaching desired shifts per month
|
||||||
|
|
||||||
let score = (p.isMale ? weights.gender : 0) -
|
let score = (p.isMale ? weights.gender : 0) -
|
||||||
(p.desiredCompletion * weights.desiredCompletion) +
|
(p.desiredCompletion * weights.desiredCompletion) +
|
||||||
((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
|
((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
|
||||||
|
@ -730,10 +730,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
Генерирай смени </button>
|
Генерирай смени </button>
|
||||||
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 1)}>
|
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 1)}>
|
||||||
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
||||||
Генерирай смени 2 </button>
|
Генерирай смени 2 <span className="text-yellow-500 ml-1 text-xs">✨</span></button>
|
||||||
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 2)}>
|
{/* ✧✨ */}
|
||||||
|
{/* <button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 2)}>
|
||||||
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
||||||
Генерирай смени 3 </button>
|
Генерирай смени 3 </button> */}
|
||||||
|
|
||||||
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100"
|
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100"
|
||||||
onClick={() => openConfirmModal(
|
onClick={() => openConfirmModal(
|
||||||
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
|
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
|
||||||
|
Reference in New Issue
Block a user