data.filterPublishers can filter by ID,
api/getPublisherInfo uses dataHelper.filterPubs instead of api.filterPublishers because it handles repeating avs and calcs statistics: shiftgenerate has fn to rank pubs based on weights. stop message every page load; other fixes
This commit is contained in:
@ -147,9 +147,17 @@ export default async function handler(req, res) {
|
|||||||
res.status(200).json(events);
|
res.status(200).json(events);
|
||||||
|
|
||||||
case "getPublisherInfo":
|
case "getPublisherInfo":
|
||||||
let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", null, req.query.assignments || true, req.query.availabilities || true, false, req.query.id);
|
//let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", null, req.query.assignments || true, req.query.availabilities || true, false, req.query.id);
|
||||||
res.status(200).json(pubs[0]);
|
let pubs = await dataHelper.filterPublishersNew("id,firstName,lastName,email,isActive,assignments,availabilities", day, false, true, false, true, false, req.query.id);
|
||||||
|
res.status(200).json(pubs[0] || {});
|
||||||
break;
|
break;
|
||||||
|
case "filterPublishersNew":
|
||||||
|
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
|
||||||
|
let results = await filterPublishersNew_Available(req.query.select, day,
|
||||||
|
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities, req.query.id);
|
||||||
|
res.status(200).json(results);
|
||||||
|
break;
|
||||||
|
|
||||||
case "getMonthlyStatistics":
|
case "getMonthlyStatistics":
|
||||||
let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day);
|
let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day);
|
||||||
res.status(200).json(allpubs);
|
res.status(200).json(allpubs);
|
||||||
@ -168,12 +176,6 @@ export default async function handler(req, res) {
|
|||||||
//!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName)));
|
//!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName)));
|
||||||
res.status(200).json(publishers);
|
res.status(200).json(publishers);
|
||||||
break;
|
break;
|
||||||
case "filterPublishersNew":
|
|
||||||
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
|
|
||||||
let results = await filterPublishersNew_Available(req.query.select, day,
|
|
||||||
common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities);
|
|
||||||
res.status(200).json(results);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// find publisher by full name or email
|
// find publisher by full name or email
|
||||||
case "findPublisher":
|
case "findPublisher":
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
import axiosServer from '../../src/axiosServer';
|
import axiosServer from '../../src/axiosServer';
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
|
import { set, format, addDays } from 'date-fns';
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
|
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
|
||||||
import { levenshteinEditDistance } from "levenshtein-edit-distance";
|
import { levenshteinEditDistance } from "levenshtein-edit-distance";
|
||||||
@ -465,7 +467,29 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
|
|
||||||
// ### COPIED TO shift api (++) ###
|
// ### 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) {
|
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) {
|
||||||
|
|
||||||
let missingPublishers = [];
|
let missingPublishers = [];
|
||||||
let publishersWithChangedPref = [];
|
let publishersWithChangedPref = [];
|
||||||
|
|
||||||
@ -494,6 +518,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
let dayNr = 1;
|
let dayNr = 1;
|
||||||
let weekNr = 1;
|
let weekNr = 1;
|
||||||
|
|
||||||
|
|
||||||
if (forDay) {
|
if (forDay) {
|
||||||
day = monthInfo.date;
|
day = monthInfo.date;
|
||||||
endDate.setDate(monthInfo.date.getDate() + 1);
|
endDate.setDate(monthInfo.date.getDate() + 1);
|
||||||
@ -503,6 +528,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
|
|
||||||
let publishersThisWeek = [];
|
let publishersThisWeek = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 0. generate shifts and assign publishers from the previous month if still available
|
// 0. generate shifts and assign publishers from the previous month if still available
|
||||||
while (day < endDate) {
|
while (day < endDate) {
|
||||||
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);
|
||||||
@ -542,8 +569,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
|
|
||||||
if (shiftLastMonthSameDay) {
|
if (shiftLastMonthSameDay) {
|
||||||
for (let assignment of shiftLastMonthSameDay.assignments) {
|
for (let assignment of shiftLastMonthSameDay.assignments) {
|
||||||
let publisher = assignment.publisher;
|
let publisher = assignment.originalPublisher ?? assignment.publisher;
|
||||||
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName);
|
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName + assignment.originalPublisher ? " (original)" : "");
|
||||||
let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr);
|
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));
|
console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime));
|
||||||
|
|
||||||
@ -554,28 +581,17 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut
|
isWithTransport: availability.isWithTransportIn || availability.isWithTransportOut
|
||||||
});
|
});
|
||||||
publishersThisWeek.push(publisher.id);
|
publishersThisWeek.push(publisher.id);
|
||||||
|
updateRegistry(publisher.id, day, weekNr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
|
let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
|
||||||
//ToDo: check if getAvailablePublishersForShift is working correctly. It seems not to!
|
//ToDo: check if getAvailablePublishersForShift is working correctly. It seems not to!
|
||||||
let availablePublishers = await getAvailablePublishersForShiftNew(shiftStart, shiftEnd, availablePubsForTheDay, publishersThisWeek);
|
let availablePublishers = await getAvailablePublishersForShiftNew(shiftStart, shiftEnd, availablePubsForTheDay, publishersThisWeek);
|
||||||
|
|
||||||
console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length);
|
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({
|
const createdShift = await prisma.shift.create({
|
||||||
data: {
|
data: {
|
||||||
startTime: shiftStart,
|
startTime: shiftStart,
|
||||||
@ -634,7 +650,7 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
|
|
||||||
let publishersToday = [];
|
let publishersToday = [];
|
||||||
|
|
||||||
// 2. First pass - prioritize shifts with transport where it is needed
|
// Second pass - prioritize shifts with transport where it is needed
|
||||||
console.log(" second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year);
|
console.log(" second pass - fix transports " + monthInfo.monthName + " " + monthInfo.year);
|
||||||
day = new Date(monthInfo.firstMonday);
|
day = new Date(monthInfo.firstMonday);
|
||||||
dayNr = 1;
|
dayNr = 1;
|
||||||
@ -646,8 +662,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
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 = 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({
|
let publishersToday = await prisma.assignment.findMany({
|
||||||
where: {
|
where: {
|
||||||
shift: {
|
shift: {
|
||||||
@ -662,7 +676,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
},
|
},
|
||||||
}).then((assignments) => assignments.map(a => a.publisherId));
|
}).then((assignments) => assignments.map(a => a.publisherId));
|
||||||
|
|
||||||
|
|
||||||
let transportShifts = shifts.filter(s => s.requiresTransport);
|
let transportShifts = shifts.filter(s => s.requiresTransport);
|
||||||
transportShifts[0].transportIn = true;
|
transportShifts[0].transportIn = true;
|
||||||
if (transportShifts.length > 1) {
|
if (transportShifts.length > 1) {
|
||||||
@ -671,7 +684,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
// if there are no transport yet:
|
// if there are no transport yet:
|
||||||
// transportShifts.forEach(async shift => {
|
// transportShifts.forEach(async shift => {
|
||||||
for (const shift of transportShifts) {
|
for (const shift of transportShifts) {
|
||||||
//todo: replace that with transport check
|
|
||||||
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
|
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
|
||||||
let transportCapable = shift.assignments.filter(a => a.isWithTransport);
|
let transportCapable = shift.assignments.filter(a => a.isWithTransport);
|
||||||
let tramsportCapableMen = transportCapable.filter(a => a.publisher.isMale);
|
let tramsportCapableMen = transportCapable.filter(a => a.publisher.isMale);
|
||||||
@ -693,8 +705,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
});
|
});
|
||||||
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
|
availablePublishers = await FilterInappropriatePublishers([...availablePublishers], publishersToday, shift);
|
||||||
// rank publishers based on different factors
|
// rank publishers based on different factors
|
||||||
let rankedPublishersOld = await RankPublishersForShift([...availablePublishers])
|
// let rankedPublishersOld = await RankPublishersForShift([...availablePublishers])
|
||||||
let rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers])
|
let rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
||||||
if (rankedPublishers.length > 0) {
|
if (rankedPublishers.length > 0) {
|
||||||
const newAssignment = await prisma.assignment.create({
|
const newAssignment = await prisma.assignment.create({
|
||||||
data: {
|
data: {
|
||||||
@ -716,8 +728,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
|
|
||||||
shift.assignments.push(newAssignment);
|
shift.assignments.push(newAssignment);
|
||||||
publishersToday.push(rankedPublishers[0].id);
|
publishersToday.push(rankedPublishers[0].id);
|
||||||
|
updateRegistry(rankedPublishers[0].id, day, weekNr);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -725,10 +737,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
day.setDate(day.getDate() + 1);
|
day.setDate(day.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill the rest of the shifts
|
||||||
|
let goal = 1;
|
||||||
// 3. next passes - fill the rest of the shifts
|
|
||||||
let goal = 1; // 4 to temporary skip
|
|
||||||
while (goal <= 4) {
|
while (goal <= 4) {
|
||||||
console.log("#".repeat(50));
|
console.log("#".repeat(50));
|
||||||
console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year);
|
console.log("Filling shifts with " + goal + " publishers " + monthInfo.monthName + " " + monthInfo.year);
|
||||||
@ -741,7 +751,6 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
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 = shifts.flatMap(s => s.assignments.map(a => a.publisher?.id));
|
|
||||||
let publishersToday = await prisma.assignment.findMany({
|
let publishersToday = await prisma.assignment.findMany({
|
||||||
where: {
|
where: {
|
||||||
shift: {
|
shift: {
|
||||||
@ -760,14 +769,15 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers");
|
console.log("" + day.toLocaleDateString() + " " + shiftsToFill.length + " shifts with less than " + goal + " publishers");
|
||||||
|
|
||||||
for (const shift of shiftsToFill) {
|
for (const shift of shiftsToFill) {
|
||||||
|
console.log("Filling shift " + shift.name + " with " + goal + " publishers");
|
||||||
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
|
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
|
||||||
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', shift.startTime, true, false, false, true, false);
|
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);
|
let availablePublishers = await FilterInappropriatePublishers([...availablePubsForTheShift], publishersToday, shift);
|
||||||
|
let rankedPublishers;
|
||||||
shift.availablePublishers = availablePublishers.length;
|
rankedPublishers = await RankPublishersForShiftOld([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
||||||
let rankedPublishers = await RankPublishersForShift([...availablePublishers])
|
//rankedPublishers = await RankPublishersForShiftWeighted([...availablePublishers], scheduledPubsPerDayAndWeek, day, weekNr);
|
||||||
if (rankedPublishers.length == 0) {
|
if (rankedPublishers.length == 0) {
|
||||||
console.log("No available publishers for shift " + shift.name);
|
console.log("No available publishers for shift " + shift.name);
|
||||||
} else if (rankedPublishers.length > 0) {
|
} else if (rankedPublishers.length > 0) {
|
||||||
@ -790,8 +800,8 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
});
|
});
|
||||||
shift.assignments.push(newAssignment);
|
shift.assignments.push(newAssignment);
|
||||||
publishersToday.push(rankedPublishers[0].id);
|
publishersToday.push(rankedPublishers[0].id);
|
||||||
|
updateRegistry(rankedPublishers[0].id, day, weekNr);
|
||||||
|
|
||||||
//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);
|
let familyMembers = availablePubsForTheShift.filter(p => p.familyHeadId && p.familyHeadId === rankedPublishers[0].familyHeadId);
|
||||||
if (familyMembers.length > 0) {
|
if (familyMembers.length > 0) {
|
||||||
familyMembers.forEach(async familyMember => {
|
familyMembers.forEach(async familyMember => {
|
||||||
@ -815,18 +825,18 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
});
|
});
|
||||||
shift.assignments.push(newAssignment);
|
shift.assignments.push(newAssignment);
|
||||||
publishersToday.push(familyMember.id);
|
publishersToday.push(familyMember.id);
|
||||||
|
updateRegistry(familyMember.id, day, weekNr);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
day.setDate(day.getDate() + 1);
|
day.setDate(day.getDate() + 1);
|
||||||
}
|
}
|
||||||
goal += 1
|
goal += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!forDay) {
|
if (!forDay) {
|
||||||
@ -845,11 +855,9 @@ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, auto
|
|||||||
async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) {
|
async function FilterInappropriatePublishers(availablePublishers, pubsToExclude, shift) {
|
||||||
//ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later.
|
//ToDo: Optimization: store number of publishers, so we process the shifts from least to most available publishers later.
|
||||||
let goodPublishers = availablePublishers.filter(p => {
|
let goodPublishers = availablePublishers.filter(p => {
|
||||||
|
|
||||||
const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id);
|
const isNotAssigned = !shift.assignments.some(a => a.publisher?.id === p.id);
|
||||||
const isNotAssignedToday = !pubsToExclude.includes(p.id);
|
const isNotAssignedToday = !pubsToExclude.includes(p.id);
|
||||||
const isAssignedEnough = p.currentMonthAssignments >= p.desiredShiftsPerMonth;
|
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 isNotAssigned && isNotAssignedToday && !isAssignedEnough;
|
||||||
});
|
});
|
||||||
return goodPublishers;
|
return goodPublishers;
|
||||||
@ -865,7 +873,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 RankPublishersForShift(publishers) {
|
async function RankPublishersForShiftOld(publishers, scheduledPubsPerDayAndWeek, currentDay) {
|
||||||
publishers.forEach(p => {
|
publishers.forEach(p => {
|
||||||
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
|
p.DesiredMinusCurrent = p.desiredShiftsPerMonth - p.currentMonthAssignments;
|
||||||
});
|
});
|
||||||
@ -874,22 +882,54 @@ async function RankPublishersForShift(publishers) {
|
|||||||
// males first (descending)
|
// males first (descending)
|
||||||
if (a.isMale && !b.isMale) return -1;
|
if (a.isMale && !b.isMale) return -1;
|
||||||
|
|
||||||
// desired completion (normalized 0%=0 - 100%=1) ; lower first
|
// 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 desiredCompletionA = a.currentMonthAssignments / a.desiredShiftsPerMonth;
|
||||||
const desiredCompletionB = b.currentMonthAssignments / b.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}`);
|
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 (scheduledPubsPerDayAndWeek[previousDayKey]?.some(pubId => pubId === a.id)) {
|
||||||
|
// adjustedCompletionA *= penalty;
|
||||||
|
// }
|
||||||
|
// if (scheduledPubsPerDayAndWeek[previousDayKey]?.some(pubId => pubId === b.id)) {
|
||||||
|
// adjustedCompletionB *= penalty;
|
||||||
|
// }
|
||||||
|
// if (scheduledPubsPerDayAndWeek[nextDayKey]?.some(pubId => pubId === a.id)) {
|
||||||
|
// adjustedCompletionA *= penalty;
|
||||||
|
// }
|
||||||
|
// if (scheduledPubsPerDayAndWeek[nextDayKey]?.some(pubId => pubId === b.id)) {
|
||||||
|
// adjustedCompletionB *= penalty;
|
||||||
|
// }
|
||||||
|
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)
|
// less available first (ascending)
|
||||||
const availabilityDifference = a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
|
const availabilityDifference = a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
|
||||||
if (availabilityDifference !== 0) return availabilityDifference;
|
if (availabilityDifference !== 0) return availabilityDifference;
|
||||||
|
|
||||||
|
|
||||||
// less assigned first (ascending)
|
// less assigned first (ascending)
|
||||||
return a.currentMonthAssignments - b.currentMonthAssignments;
|
return a.currentMonthAssignments - b.currentMonthAssignments;
|
||||||
});
|
});
|
||||||
@ -897,8 +937,9 @@ async function RankPublishersForShift(publishers) {
|
|||||||
return ranked;
|
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.
|
// 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) {
|
async function RankPublishersForShiftWeighted(publishers, scheduledPubsPerDayAndWeek, currentDay, currentWeekNr) {
|
||||||
// Define weights for each criterion
|
// Define weights for each criterion
|
||||||
const weights = {
|
const weights = {
|
||||||
gender: 2,
|
gender: 2,
|
||||||
@ -919,19 +960,33 @@ async function RankPublishersForShiftWeighted(publishers) {
|
|||||||
p.desiredCompletion = p.currentMonthAssignments / p.desiredShiftsPerMonth;
|
p.desiredCompletion = p.currentMonthAssignments / p.desiredShiftsPerMonth;
|
||||||
});
|
});
|
||||||
|
|
||||||
let ranked = publishers.sort((a, b) => {
|
const calculateScore = (p) => {
|
||||||
// Calculate weighted score for each publisher
|
let score = (p.isMale ? weights.gender : 0) -
|
||||||
const scoreA = (a.isMale ? weights.gender : 0) -
|
(p.desiredCompletion * weights.desiredCompletion) +
|
||||||
(a.desiredCompletion * weights.desiredCompletion) +
|
((1 - p.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
|
||||||
((1 - a.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
|
(p.currentMonthAssignments * weights.lastMonthCompletion) -
|
||||||
(a.currentMonthAssignments * weights.lastMonthCompletion) -
|
(p.currentMonthAssignments * weights.currentAssignments);
|
||||||
(a.currentMonthAssignments * weights.currentAssignments);
|
|
||||||
|
|
||||||
const scoreB = (b.isMale ? weights.gender : 0) -
|
// Apply penalties based on proximity to current day
|
||||||
(b.desiredCompletion * weights.desiredCompletion) +
|
for (let i = 1; i <= 6; i++) {
|
||||||
((1 - b.currentMonthAvailabilityHoursCount / 24) * weights.availability) +
|
const previousDayKey = common.getISODateOnly(addDays(currentDay, -i));
|
||||||
(b.currentMonthAssignments * weights.lastMonthCompletion) -
|
const nextDayKey = common.getISODateOnly(addDays(currentDay, i));
|
||||||
(b.currentMonthAssignments * weights.currentAssignments);
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (flattenRegistry(nextDayKey).includes(p.id)) {
|
||||||
|
score *= penalty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ranked = publishers.sort((a, b) => {
|
||||||
|
const scoreA = calculateScore(a);
|
||||||
|
const scoreB = calculateScore(b);
|
||||||
|
|
||||||
return scoreB - scoreA; // Sort descending by score
|
return scoreB - scoreA; // Sort descending by score
|
||||||
});
|
});
|
||||||
@ -943,6 +998,7 @@ async function RankPublishersForShiftWeighted(publishers) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function DeleteShiftsForMonth(monthInfo) {
|
async function DeleteShiftsForMonth(monthInfo) {
|
||||||
try {
|
try {
|
||||||
const prisma = common.getPrismaClient();
|
const prisma = common.getPrismaClient();
|
||||||
|
@ -362,7 +362,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
const newAssignment = {
|
const newAssignment = {
|
||||||
publisher: { connect: { id: publisher.id } },
|
publisher: { connect: { id: publisher.id } },
|
||||||
shift: { connect: { id: shiftId } },
|
shift: { connect: { id: shiftId } },
|
||||||
isActive: true,
|
|
||||||
isConfirmed: true
|
isConfirmed: true
|
||||||
};
|
};
|
||||||
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
||||||
@ -842,7 +841,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
|
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
|
||||||
});
|
});
|
||||||
const dayShifts = weekShifts.map(shift => {
|
const dayShifts = weekShifts.map(shift => {
|
||||||
const isAvailable = publisher.availabilities.some(avail =>
|
const isAvailable = publisher.availabilities?.some(avail =>
|
||||||
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
|
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
|
||||||
);
|
);
|
||||||
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
|
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
|
||||||
@ -863,7 +862,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
|
|
||||||
const hasAssignment = (shiftId) => {
|
const hasAssignment = (shiftId) => {
|
||||||
// return publisher.assignments.some(ass => ass.shift.id == shiftId);
|
// return publisher.assignments.some(ass => ass.shift.id == shiftId);
|
||||||
return publisher.assignments.some(ass => {
|
return publisher.assignments?.some(ass => {
|
||||||
console.log(`Comparing: ${ass.shift.id} to ${shiftId}: ${ass.shift.id === shiftId}`);
|
console.log(`Comparing: ${ass.shift.id} to ${shiftId}: ${ass.shift.id === shiftId}`);
|
||||||
return ass.shift.id === shiftId;
|
return ass.shift.id === shiftId;
|
||||||
});
|
});
|
||||||
|
@ -35,9 +35,9 @@ const SchedulePage = () => {
|
|||||||
}, []); // Empty dependency array means this effect runs once on component mount
|
}, []); // Empty dependency array means this effect runs once on component mount
|
||||||
|
|
||||||
// temporary alert for the users
|
// temporary alert for the users
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
|
// alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -28,9 +28,9 @@ export default function MySchedulePage({ assignments }) {
|
|||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
// temporary alert for the users
|
// temporary alert for the users
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
|
// alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
if (status === "loading") {
|
if (status === "loading") {
|
||||||
return <div className="flex justify-center items-center h-screen">Зареждане...</div>;
|
return <div className="flex justify-center items-center h-screen">Зареждане...</div>;
|
||||||
|
@ -56,7 +56,7 @@ export default function DashboardPage({ initialItems, initialUserId, cartEvents,
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//if (newLogin === 'true')
|
//if (newLogin === 'true')
|
||||||
{
|
{
|
||||||
alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
|
// alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
|
||||||
const currentPath = router.pathname;
|
const currentPath = router.pathname;
|
||||||
router.replace(currentPath, undefined, { shallow: true }); // Removes the query without affecting the history
|
router.replace(currentPath, undefined, { shallow: true }); // Removes the query without affecting the history
|
||||||
}
|
}
|
||||||
@ -281,6 +281,7 @@ export const getServerSideProps = async (context) => {
|
|||||||
endTime: 'desc'
|
endTime: 'desc'
|
||||||
}
|
}
|
||||||
})).endTime;
|
})).endTime;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
initialItems: items,
|
initialItems: items,
|
||||||
|
@ -221,11 +221,12 @@ async function getAvailabilities(userId) {
|
|||||||
*
|
*
|
||||||
* @returns {Promise<Array>} Returns a promise that resolves to an array of publishers with filtered data according to the specified criteria.
|
* @returns {Promise<Array>} Returns a promise that resolves to an array of publishers with filtered data according to the specified criteria.
|
||||||
*/
|
*/
|
||||||
async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false) {
|
async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false, id = null) {
|
||||||
|
|
||||||
const prisma = common.getPrismaClient();
|
const prisma = common.getPrismaClient();
|
||||||
filterDate = new Date(filterDate); // Convert to date object if not already
|
if (filterDate !== null) {
|
||||||
|
filterDate = new Date(filterDate); // Convert to date object if not already
|
||||||
|
}
|
||||||
const monthInfo = common.getMonthDatesInfo(filterDate);
|
const monthInfo = common.getMonthDatesInfo(filterDate);
|
||||||
let prevMnt = new Date(filterDate)
|
let prevMnt = new Date(filterDate)
|
||||||
prevMnt.setMonth(prevMnt.getMonth() - 1);
|
prevMnt.setMonth(prevMnt.getMonth() - 1);
|
||||||
@ -262,9 +263,20 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
|
|||||||
|
|
||||||
let filterTimeFrom = new Date(filterDate)
|
let filterTimeFrom = new Date(filterDate)
|
||||||
let filterTimeTo = new Date(filterDate);
|
let filterTimeTo = new Date(filterDate);
|
||||||
|
//check if filterDate is valid date
|
||||||
|
if (isNaN(filterDate.getTime())) {
|
||||||
|
console.error("Invalid date: " + filterDate);
|
||||||
|
filterTimeFrom = new Date(2024, 1, 1);
|
||||||
|
noEndDateFilter = true;
|
||||||
|
isForTheMonth = false
|
||||||
|
}
|
||||||
|
|
||||||
let isDayFilter = true;
|
let isDayFilter = true;
|
||||||
let whereClause = {};
|
let whereClause = {};
|
||||||
|
if (id) {
|
||||||
|
whereClause.id = String(id)
|
||||||
|
|
||||||
|
}
|
||||||
if (isForTheMonth) {
|
if (isForTheMonth) {
|
||||||
var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber
|
var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber
|
||||||
|
|
||||||
@ -282,59 +294,65 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
|
|||||||
filterTimeTo.setHours(23, 59, 59, 999);
|
filterTimeTo.setHours(23, 59, 59, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
whereClause["availabilities"] = {
|
if (filterDate) { // if no date is provided, we don't filter by date
|
||||||
some: {
|
whereClause["availabilities"] = {
|
||||||
OR: [
|
some: {
|
||||||
// Check if dayOfMonth is not null and startTime is after monthInfo.firstMonday (Assignments on specific days AND time)
|
OR: [
|
||||||
{
|
// ONE TIME AVAILABILITIES
|
||||||
//dayOfMonth: { not: null },
|
// Check if dayOfMonth is not null and startTime is after monthInfo.firstMonday (Assignments on specific days AND time)
|
||||||
startTime: { gte: filterTimeFrom },
|
{
|
||||||
// endTime: { lte: monthInfo.lastSunday }
|
//dayOfMonth: { not: null },
|
||||||
},
|
startTime: { gte: filterTimeFrom },
|
||||||
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
// endTime: { lte: monthInfo.lastSunday }
|
||||||
{
|
},
|
||||||
// dayOfMonth: null,
|
// REPEATING WEEKLY AVAILABILITIES
|
||||||
// startTime: { gte: filterTimeFrom },
|
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
|
||||||
AND: [
|
{
|
||||||
{ dayOfMonth: null },
|
// dayOfMonth: null,
|
||||||
{ startTime: { lte: filterTimeTo } }, // startTime included
|
// startTime: { gte: filterTimeFrom },
|
||||||
{
|
AND: [
|
||||||
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
{ dayOfMonth: null },
|
||||||
{ endDate: { gte: filterTimeFrom } }, // endDate included
|
// moved down to conditional filters
|
||||||
{ endDate: null }
|
{ startTime: { lte: filterTimeTo } }, // startTime included
|
||||||
]
|
{
|
||||||
}
|
OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever
|
||||||
]
|
{ endDate: { gte: filterTimeFrom } }, // endDate included
|
||||||
}
|
{ endDate: null }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* FILTERS
|
|
||||||
1. exact time
|
|
||||||
2. exact date
|
|
||||||
3. the month
|
|
||||||
4. from start date only
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (noEndDateFilter) {
|
/* FILTERS
|
||||||
isDayFilter = false;
|
1. exact time
|
||||||
}
|
2. exact date
|
||||||
else {
|
3. the month
|
||||||
whereClause["availabilities"].some.OR[0].endTime = { lte: filterTimeTo };
|
4. from start date only
|
||||||
if (isForTheMonth) {
|
*/
|
||||||
// no dayofweek or time filters here
|
|
||||||
|
if (noEndDateFilter) {
|
||||||
|
isDayFilter = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate);
|
whereClause["availabilities"].some.OR[0].endTime = { lte: filterTimeTo };
|
||||||
whereClause["availabilities"].some.OR[1].dayofweek = dayOfWeekEnum;
|
if (isForTheMonth) {
|
||||||
//NOTE: we filter by date after we calculate the correct dates post query
|
// no dayofweek or time filters here
|
||||||
if (isExactTime) {
|
|
||||||
//if exact time we need the availability to be starting on or before start of the shift and ending on or after the end of the shift
|
|
||||||
whereClause["availabilities"].some.OR[0].startTime = { lte: filterTimeFrom };
|
|
||||||
whereClause["availabilities"].some.OR[0].endTime = { gte: filterTimeTo };
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate);
|
||||||
|
whereClause["availabilities"].some.OR[1].dayofweek = dayOfWeekEnum;
|
||||||
|
//NOTE: we filter by date after we calculate the correct dates post query
|
||||||
|
if (isExactTime) {
|
||||||
|
//if exact time we need the availability to be starting on or before start of the shift and ending on or after the end of the shift
|
||||||
|
whereClause["availabilities"].some.OR[0].startTime = { lte: filterTimeFrom };
|
||||||
|
whereClause["availabilities"].some.OR[0].endTime = { gte: filterTimeTo };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1179,6 +1197,7 @@ async function FindPublisherAvailability(publisherId, startDate, endDate, dayOfW
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { filter } = require('jszip');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
async function runSqlFile(filePath) {
|
async function runSqlFile(filePath) {
|
||||||
|
Reference in New Issue
Block a user